import schedule import threading import time from xt_trader import XtTrader from flask import Flask, request, abort, jsonify from flask_limiter import Limiter from flask_limiter.util import get_remote_address from config import Config import logging import os from logging.handlers import RotatingFileHandler import concurrent.futures from concurrent.futures import TimeoutError import json import atexit from simulation_trader import SimulationTrader import datetime from xtquant import xtconstant # 策略仓位管理 strategy_positions = {} # 存储各策略持仓 strategy_trades = {} # 存储各策略交易记录 pending_orders = {} # 存储未完成委托 # 全局交易实例(采用单例模式) _sim_trader_instance = None # 模拟交易实例(单例) _real_trader_instance = None # 实盘交易实例(单例) # 获取交易实例 def get_trader(use_sim_trader=False): """获取交易实例 - 采用单例模式 Args: use_sim_trader (bool): 是否强制使用模拟交易,True表示必定返回模拟交易实例 Returns: 返回交易实例,根据参数和配置决定是模拟交易还是实盘交易 """ global _sim_trader_instance, _real_trader_instance # 如果强制使用模拟交易,返回模拟交易单例 if use_sim_trader: # 如果模拟交易实例不存在,创建一个 if _sim_trader_instance is None: _sim_trader_instance = SimulationTrader() return _sim_trader_instance # 如果配置为仅模拟交易,返回模拟交易单例 if Config.SIMULATION_ONLY: # 如果模拟交易实例不存在,创建一个 if _sim_trader_instance is None: _sim_trader_instance = SimulationTrader() return _sim_trader_instance # 判断当前是否为交易时间 now = datetime.datetime.now() current_time = now.time() # 是否在交易时间段内(9:30-11:30, 13:00-15:00) morning_start = datetime.time(9, 30) morning_end = datetime.time(11, 30) afternoon_start = datetime.time(13, 0) afternoon_end = datetime.time(15, 0) is_trading_hour = (morning_start <= current_time <= morning_end) or (afternoon_start <= current_time <= afternoon_end) # 尝试导入chinese_calendar判断是否为交易日 try: from chinese_calendar import is_workday, is_holiday is_trading_day = is_workday(now) and not is_holiday(now) except ImportError: # 如果无法导入chinese_calendar,则简单地用工作日判断 is_trading_day = now.weekday() < 5 # 0-4 为周一至周五 # 如果不是交易日或不在交易时间内,返回模拟交易单例 if not is_trading_day or not is_trading_hour: logger.info(f"当前非交易时段 - 日期: {now.date()}, 时间: {current_time}, 使用模拟交易") # 如果模拟交易实例不存在,创建一个 if _sim_trader_instance is None: _sim_trader_instance = SimulationTrader() return _sim_trader_instance # 否则返回真实交易单例(如果存在)或创建一个新的 if _real_trader_instance is None: _real_trader_instance = XtTrader() # 检查交易实例是否已登录,如果未登录则进行登录 if not _real_trader_instance.is_logged_in(): logger.info("创建新的XtTrader实例并登录") _real_trader_instance.login() return _real_trader_instance # 配置日志 def setup_logger(): log_dir = Config.LOG_DIR try: if not os.path.exists(log_dir): os.makedirs(log_dir) logger = logging.getLogger('trade_server') logger.info(f"Created logs directory at: {os.path.abspath(log_dir)}") except Exception as e: print(f"Error creating logs directory: {str(e)}") # 如果无法创建目录,使用当前目录 log_dir = "." logger = logging.getLogger('trade_server') logger.setLevel(getattr(logging, Config.LOG_LEVEL)) # 确保没有重复的处理器 for handler in logger.handlers[:]: logger.removeHandler(handler) # 文件处理器 file_handler = RotatingFileHandler( os.path.join(log_dir, 'trade_server.log'), maxBytes=Config.LOG_MAX_BYTES, backupCount=Config.LOG_BACKUP_COUNT ) file_handler.setFormatter(logging.Formatter(Config.LOG_FORMAT)) logger.addHandler(file_handler) # 添加控制台处理器 console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter(Config.LOG_FORMAT)) logger.addHandler(console_handler) return logger logger = setup_logger() def run_daily(time_str, job_func): """设置每天在指定时间运行的任务 Args: time_str: 运行时间,格式为"HH:MM" job_func: 要运行的函数 """ # 不再区分周一到周五,而是每天执行 # 交易日判断逻辑已移到get_trader函数中 schedule.every().day.at(time_str).do(job_func) # 策略持仓管理辅助函数 def update_strategy_position(strategy_name, code, direction, amount): """更新策略持仓 Args: strategy_name: 策略名称 code: 股票代码 direction: 'buy'或'sell' amount: 交易数量 """ if not strategy_name: return # 确保策略在字典中 if strategy_name not in strategy_positions: strategy_positions[strategy_name] = {} try: # 获取交易实例持仓情况(不论真实还是模拟) current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 actual_positions = current_trader.get_positions() code_position = next((pos for pos in actual_positions if pos.get('stock_code') == code), None) # 记录实际持仓总量 actual_total = code_position.get('volume', 0) if code_position else 0 actual_can_use = code_position.get('can_use_volume', 0) if code_position else 0 logger.info(f"实际持仓 - 代码: {code}, 总量: {actual_total}, 可用: {actual_can_use}") # 如果股票代码在持仓字典中不存在,初始化它 if code not in strategy_positions[strategy_name]: strategy_positions[strategy_name][code] = { 'total_amount': 0, 'closeable_amount': 0 } # 直接使用实际持仓数据更新策略持仓 strategy_positions[strategy_name][code]['total_amount'] = actual_total strategy_positions[strategy_name][code]['closeable_amount'] = actual_can_use logger.info(f"更新策略持仓 - 策略: {strategy_name}, 代码: {code}, 方向: {direction}, 数量: {amount}, 总量: {strategy_positions[strategy_name][code]['total_amount']}, 可用: {strategy_positions[strategy_name][code]['closeable_amount']}") except Exception as e: logger.error(f"获取实际持仓失败: {str(e)}") # 异常情况下只记录错误,不尝试更新持仓 # 移除total_amount为0的持仓 if code in strategy_positions[strategy_name] and strategy_positions[strategy_name][code]['total_amount'] <= 0: del strategy_positions[strategy_name][code] def update_pending_orders(): """更新未完成委托状态""" try: # 获取今日委托 current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 today_entrusts = current_trader.get_today_entrust() # 更新委托状态 for order_id, order_info in list(pending_orders.items()): entrust = next((e for e in today_entrusts if e.get('order_id') == order_id), None) if entrust: if entrust.get('order_status') in [xtconstant.ORDER_SUCCEEDED, xtconstant.ORDER_PART_SUCC]: # 成交量计算 traded_amount = int(entrust.get('traded_volume', 0)) # 更新策略持仓 update_strategy_position( order_info['strategy_name'], order_info['code'], order_info['direction'], traded_amount ) # 如果完全成交,从待处理列表中移除 if entrust.get('order_status') == xtconstant.ORDER_SUCCEEDED: del pending_orders[order_id] # 如果已撤单、废单等终态,也从待处理列表中移除 elif entrust.get('order_status') in [xtconstant.ORDER_CANCELED, xtconstant.ORDER_JUNK]: del pending_orders[order_id] except Exception as e: logger.error(f"更新未完成委托状态失败: {str(e)}") def add_pending_order(order_id, strategy_name, code, price, amount, direction): """添加未完成委托 Args: order_id: 委托编号 strategy_name: 策略名称 code: 股票代码 price: 委托价格 amount: 委托数量 direction: 交易方向,'buy'或'sell' """ if not order_id or order_id == 'simulation': return # 添加到未完成委托列表 pending_orders[order_id] = { 'strategy_name': strategy_name, 'code': code, 'price': price, 'amount': amount, 'direction': direction, 'created_time': time.time() } # 同时记录到交易历史 if strategy_name: if strategy_name not in strategy_trades: strategy_trades[strategy_name] = [] strategy_trades[strategy_name].append({ 'time': time.strftime('%Y-%m-%d %H:%M:%S'), 'type': direction, 'code': code, 'price': price, 'amount': amount, 'order_id': order_id, 'status': 'pending' }) logger.info(f"添加未完成委托: {order_id}, 策略: {strategy_name}, 代码: {code}, 方向: {direction}") def clean_timeout_orders(): """清理超时委托""" current_time = time.time() for order_id, order_info in list(pending_orders.items()): # 超过24小时的委托视为超时 if current_time - order_info['created_time'] > 24 * 60 * 60: del pending_orders[order_id] def load_strategy_data(): """加载策略数据""" global strategy_positions, strategy_trades, pending_orders try: if os.path.exists('strategy_data.json'): with open('strategy_data.json', 'r') as f: data = json.load(f) strategy_positions = data.get('positions', {}) strategy_trades = data.get('trades', {}) pending_orders = data.get('pending_orders', {}) except Exception as e: logger.error(f"加载策略数据失败: {str(e)}") def save_strategy_data(): """保存策略数据""" try: with open('strategy_data.json', 'w') as f: json.dump({ 'positions': strategy_positions, 'trades': strategy_trades, 'pending_orders': pending_orders }, f) except Exception as e: logger.error(f"保存策略数据失败: {str(e)}") def run_pending_tasks(): while True: try: schedule.run_pending() time.sleep(1) except Exception as e: logger.error(f"Error in scheduler: {str(e)}") # Run the task scheduler in a new thread threading.Thread(target=run_pending_tasks).start() # 初始化交易环境 get_trader().login() # 添加请求频率限制 app = Flask(__name__) # 添加策略数据相关的定期任务 schedule.every().day.at("00:01").do(clean_timeout_orders) # 每天清理超时委托 schedule.every().day.at("15:30").do(save_strategy_data) # 每天收盘后保存策略数据 # 程序启动时加载策略数据 load_strategy_data() # 程序退出时保存策略数据 atexit.register(save_strategy_data) # 使用配置文件中的时间 run_daily(Config.MARKET_OPEN_TIME, lambda: get_trader().login()) run_daily(Config.MARKET_ACTIVE_TIME, lambda: get_trader().get_balance()) run_daily(Config.MARKET_CLOSE_TIME, lambda: get_trader().logout()) @app.route("/yu/healthcheck", methods=["GET"]) def health_check(): return "ok", 200 def should_use_simulation(): """判断是否应该使用模拟交易 Returns: tuple: (should_simulate: bool, simulation_reason: str) should_simulate: 是否应该使用模拟交易 simulation_reason: 使用模拟交易的原因 """ # 直接使用get_trader()返回的实例类型判断 trader = get_trader() if isinstance(trader, SimulationTrader): # 获取原因 if Config.SIMULATION_ONLY: return True, "配置为仅模拟交易" else: now = datetime.datetime.now() return True, f"当前非交易时段 - {now.strftime('%Y-%m-%d %H:%M:%S')}" # 如果是实盘交易实例 return False, "" @app.route("/yu/buy", methods=["POST"]) def buy(): """Buy an item with given parameters.""" logger.info("Received buy request") # 每次操作前更新未完成委托状态 update_pending_orders() # Get data from request body data = request.get_json() code = data.get("code") price_str = data.get("price") amount_str = data.get("amount") strategy_name = data.get("strategy_name", "") # 新增策略名称参数,默认为空 try: if not all([code, price_str, amount_str]): raise ValueError("Missing required parameters") price = float(price_str) amount = int(amount_str) if price <= 0 or amount <= 0: raise ValueError("Price and amount must be positive") # 检查是否需要模拟交易 should_simulate, simulation_reason = should_use_simulation() if should_simulate: # 使用模拟交易 logger.info(f"使用模拟交易 - {simulation_reason} - 代码: {code}, 价格: {price}, 数量: {amount}") # 获取模拟交易实例并执行买入操作 sim_trader = get_trader(True) result = sim_trader.buy(code, price, amount) # 如果指定了策略名称,记录到策略持仓 if strategy_name: # 模拟交易立即生效,更新策略持仓 update_strategy_position(strategy_name, code, 'buy', amount) return jsonify({"success": True, "data": result}), 200 # 尝试实盘交易 logger.info(f"Executing buy order: code={code}, price={price}, amount={amount}, strategy_name={strategy_name}") try: current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 result = execute_with_timeout(current_trader.buy, Config.TRADE_TIMEOUT, code, price, amount) if result is None: # 超时时使用模拟交易 logger.warning(f"Buy order timeout after {Config.TRADE_TIMEOUT} seconds, switching to simulation mode") # 创建模拟交易实例并执行买入操作 sim_trader = get_trader(True) result = sim_trader.buy(code, price, amount) # 如果指定了策略名称,记录到策略持仓 if strategy_name: # 超时情况下,使用模拟交易,立即更新策略持仓 update_strategy_position(strategy_name, code, 'buy', amount) return jsonify({"success": True, "data": result}), 200 # 如果指定了策略名称且是真实交易 if strategy_name and 'order_id' in result and result['order_id'] != 'simulation': order_id = result['order_id'] # 添加到未完成委托 add_pending_order( order_id, strategy_name, code, price, amount, 'buy' ) # 注意:不在这里调用update_strategy_position # 持仓更新将由update_pending_orders函数处理 # 这避免了持仓更新的冗余操作 logger.info(f"Buy order result: {result}") return jsonify({"success": True, "data": result}), 200 except Exception as e: # 发生错误时使用模拟交易 logger.error(f"Buy order failed: {str(e)}, switching to simulation mode") # 创建模拟交易实例并执行买入操作 sim_trader = get_trader(True) result = sim_trader.buy(code, price, amount) # 如果指定了策略名称,记录到策略持仓 if strategy_name: # 错误情况下,使用模拟交易,立即更新策略持仓 update_strategy_position(strategy_name, code, 'buy', amount) return jsonify({"success": True, "data": result}), 200 except ValueError as e: logger.error(f"Invalid request parameters: {str(e)}") abort(400, description=str(e)) except Exception as e: logger.error(f"Error processing buy request: {str(e)}") abort(500, description="Internal server error") @app.route("/yu/sell", methods=["POST"]) def sell(): """Sell an item with given parameters.""" logger.info("Received sell request") # 每次操作前更新未完成委托状态 update_pending_orders() # Get data from request body data = request.get_json() code = data.get("code") price_str = data.get("price") amount_str = data.get("amount") strategy_name = data.get("strategy_name", "") # 新增策略名称参数,默认为空 try: if not all([code, price_str, amount_str]): raise ValueError("Missing required parameters") price = float(price_str) amount = int(amount_str) if price <= 0 or amount <= 0: raise ValueError("Price and amount must be positive") # 检查是否需要模拟交易 should_simulate, simulation_reason = should_use_simulation() if should_simulate: # 使用模拟交易 logger.info(f"使用模拟交易 - {simulation_reason} - 代码: {code}, 价格: {price}, 数量: {amount}") # 获取模拟交易实例并执行卖出操作 sim_trader = get_trader(True) result = sim_trader.sell(code, price, amount) # 如果指定了策略名称,记录到策略持仓 if strategy_name: # 模拟交易下,使用简单更新模式 update_strategy_position(strategy_name, code, 'sell', amount) return jsonify({"success": True, "data": result}), 200 # 尝试实盘交易 logger.info(f"Executing sell order: code={code}, price={price}, amount={amount}, strategy_name={strategy_name}") try: current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 result = execute_with_timeout(current_trader.sell, Config.TRADE_TIMEOUT, code, price, amount) if result is None: # 超时时使用模拟交易 logger.warning(f"Sell order timeout after {Config.TRADE_TIMEOUT} seconds, switching to simulation mode") # 创建模拟交易实例并执行卖出操作 sim_trader = get_trader(True) result = sim_trader.sell(code, price, amount) # 如果指定了策略名称,记录到策略持仓 if strategy_name: # 超时情况下,使用简单更新模式 update_strategy_position(strategy_name, code, 'sell', amount) return jsonify({"success": True, "data": result}), 200 # 如果指定了策略名称,记录到未完成委托 if strategy_name and 'order_id' in result and result['order_id'] != 'simulation': order_id = result['order_id'] # 添加到未完成委托 add_pending_order( order_id, strategy_name, code, price, amount, 'sell' ) logger.info(f"Sell order result: {result}") return jsonify({"success": True, "data": result}), 200 except Exception as e: # 发生错误时使用模拟交易 logger.error(f"Sell order failed: {str(e)}, switching to simulation mode") # 创建模拟交易实例并执行卖出操作 sim_trader = get_trader(True) result = sim_trader.sell(code, price, amount) # 如果指定了策略名称,记录到策略持仓 if strategy_name: # 错误情况下,使用简单更新模式 update_strategy_position(strategy_name, code, 'sell', amount) return jsonify({"success": True, "data": result}), 200 except ValueError as e: logger.error(f"Invalid request parameters: {str(e)}") abort(400, description=str(e)) except Exception as e: logger.error(f"Error processing sell request: {str(e)}") abort(500, description="Internal server error") @app.route("/yu/cancel/", methods=["DELETE"]) def cancel(entrust_no): logger.info(f"Received cancel request for entrust_no={entrust_no}") try: current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 result = current_trader.cancel(entrust_no) logger.info(f"Cancel result: {result}") # 如果取消成功,从pending_orders中移除该订单 if entrust_no in pending_orders: order_info = pending_orders[entrust_no] logger.info(f"从待处理委托中移除已取消订单: {entrust_no}, 代码: {order_info.get('code', 'unknown')}") del pending_orders[entrust_no] response = {"success": True, "data": result} return jsonify(response), 200 except Exception as e: logger.error(f"Error processing cancel request: {str(e)}") abort(500, description="Internal server error") @app.route("/yu/balance", methods=["GET"]) def get_balance(): """Get the balance of the account.""" logger.info("Received balance request") try: current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 balance = current_trader.get_balance() logger.info(f"Balance: {balance}") response = {"success": True, "data": balance} return jsonify(response), 200 except Exception as e: print(e) logger.error(f"Error processing balance request: {str(e)}") abort(500, description="Internal server error") @app.route("/yu/positions", methods=["GET"]) def get_positions(): """Get the positions of the account.""" logger.info("Received positions request") # 每次查询前更新未完成委托状态 update_pending_orders() try: # 获取查询参数中的策略名称 strategy_name = request.args.get("strategy_name", "") # 如果指定了策略名称,返回该策略的持仓 if strategy_name: # 获取真实账户持仓,用于计算可交易量 current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 real_positions = current_trader.get_positions() real_positions_map = {} for pos in real_positions: # 使用xt_trader返回的字段名 if 'stock_code' in pos and 'can_use_volume' in pos: real_positions_map[pos['stock_code']] = pos # 如果该策略没有记录,返回空列表 if strategy_name not in strategy_positions: logger.info(f"Strategy {strategy_name} has no positions") return jsonify({"success": True, "data": []}), 200 # 合并策略持仓和真实持仓的可交易量 result = [] for code, pos_info in strategy_positions[strategy_name].items(): # 忽略total_amount为0的持仓 if pos_info['total_amount'] <= 0: continue # 使用真实账户的可交易量作为策略的可交易量上限 real_pos = real_positions_map.get(code, {}) closeable = min(pos_info['total_amount'], real_pos.get('can_use_volume', 0)) result.append({ code: { 'total_amount': pos_info['total_amount'], 'closeable_amount': closeable } }) logger.info(f"Strategy {strategy_name} positions: {result}") return jsonify({"success": True, "data": result}), 200 # 否则返回原始持仓 current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 positions = current_trader.get_positions() logger.info(f"Positions: {positions}") response = {"success": True, "data": positions} return jsonify(response), 200 except Exception as e: logger.error(f"Error processing positions request: {str(e)}") abort(500, description="Internal server error") @app.route("/yu/todaytrades", methods=["GET"]) def get_today_trades(): """Get the today's trades of the account.""" logger.info("Received today trades request") try: current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 trades = current_trader.get_today_trades() logger.info(f"Today trades: {trades}") response = {"success": True, "data": trades} return jsonify(response), 200 except Exception as e: logger.error(f"Error processing today trades request: {str(e)}") abort(500, description="Internal server error") @app.route("/yu/todayentrust", methods=["GET"]) def get_today_entrust(): """Get the today's entrust of the account.""" logger.info("Received today entrust request") try: current_trader = get_trader() # get_trader 已经确保交易实例是登录状态的,无需再次检查 entrust = current_trader.get_today_entrust() logger.info(f"Today entrust: {entrust}") response = {"success": True, "data": entrust} return jsonify(response), 200 except Exception as e: logger.error(f"Error processing today entrust request: {str(e)}") abort(500, description="Internal server error") @app.route("/yu/clear/", methods=["DELETE"]) def clear_strategy(strategy_name): """清除指定策略的持仓管理数据""" logger.info(f"接收到清除策略持仓请求: {strategy_name}") try: if not strategy_name: raise ValueError("缺少策略名称参数") # 检查策略是否存在 if strategy_name in strategy_positions: # 从策略持仓字典中删除该策略 del strategy_positions[strategy_name] # 清除该策略的交易记录 if strategy_name in strategy_trades: del strategy_trades[strategy_name] # 清除与该策略相关的未完成委托 for order_id, order_info in list(pending_orders.items()): if order_info.get('strategy_name') == strategy_name: del pending_orders[order_id] # 保存更新后的策略数据 save_strategy_data() logger.info(f"成功清除策略持仓数据: {strategy_name}") return jsonify({"success": True, "message": f"成功清除策略 '{strategy_name}' 的持仓数据"}), 200 else: logger.info(f"策略不存在或没有持仓数据: {strategy_name}") return jsonify({"success": True, "message": f"策略 '{strategy_name}' 不存在或没有持仓数据"}), 200 except ValueError as e: logger.error(f"无效的请求参数: {str(e)}") abort(400, description=str(e)) except Exception as e: logger.error(f"清除策略持仓时出错: {str(e)}") abort(500, description="服务器内部错误") # 超时处理函数 def execute_with_timeout(func, timeout, *args, **kwargs): with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(func, *args, **kwargs) try: return future.result(timeout=timeout) except TimeoutError: return None if __name__ == "__main__": logger.info(f"Server starting on {Config.HOST}:{Config.PORT}") app.run(debug=Config.DEBUG, host=Config.HOST, port=Config.PORT)