diff --git a/README.md b/README.md index 9b4d12c..8b8b091 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Real Trader是一个量化交易执行平台,专为中国A股市场设计, - **PORT**:服务端口(默认:9527) - **HOST**:服务监听地址(默认:0.0.0.0) - **DEBUG**:调试模式(默认:False) -- **SIMULATION_ONLY**:是否仅使用模拟交易(默认:False) +- **SIMULATION_MODE**:是否仅使用模拟交易(默认:False) - **XT_ACCOUNT**:XtQuant账号 - **XT_PATH**:XtQuant路径 diff --git a/pyproject.toml b/pyproject.toml index 6d166be..fc6ec9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12.8" dependencies = [ + "black>=25.1.0", "chinese-calendar>=1.10.0", "flask>=3.1.0", "flask-limiter>=3.12", diff --git a/src/base_trader.py b/src/base_trader.py index 193a6b0..ce0c5e6 100644 --- a/src/base_trader.py +++ b/src/base_trader.py @@ -2,6 +2,8 @@ import datetime as dt from chinese_calendar import is_workday from abc import ABC, abstractmethod from logger_config import get_logger +from position_manager import PositionManager +from typing import Dict # 获取日志记录器 logger = get_logger('base_trader') @@ -11,6 +13,7 @@ class BaseTrader(ABC): def __init__(self): """初始化交易基类""" + self.position_managers: Dict[str, PositionManager] = {} pass @abstractmethod @@ -187,4 +190,37 @@ class BaseTrader(ABC): except Exception as e: logger.error(f"判断交易日期发生错误: {str(e)}") - return False \ No newline at end of file + return False + + def get_position_manager(self, strategy_name) -> PositionManager: + """获取指定策略的持仓管理器 + + Args: + strategy_name: 策略名称 + + Returns: + PositionManager: 指定策略的持仓管理器 + """ + if strategy_name not in self.position_managers: + self.position_managers[strategy_name] = PositionManager(strategy_name) + return self.position_managers[strategy_name] + + def get_all_position_managers(self) -> Dict[str, PositionManager]: + """获取所有持仓管理器""" + return self.position_managers + + def is_today(self, datetime: dt.datetime) -> bool: + """判断指定日期是否为当前日期 + + Args: + datetime: 日期时间 + + Returns: + bool: True 表示是当前日期,False 表示不是当前日期 + """ + return datetime.date() == dt.datetime.now().date() + + def clear_position_manager(self, strategy_name): + """清除指定策略的持仓管理器""" + if strategy_name in self.position_managers: + self.position_managers[strategy_name].clear() diff --git a/src/config.py b/src/config.py index c99dc22..6f4e8df 100644 --- a/src/config.py +++ b/src/config.py @@ -9,7 +9,7 @@ class Config: # Trading settings TRADE_TIMEOUT = int(os.environ.get("TRADE_TIMEOUT", 5)) # 交易超时时间(秒) - SIMULATION_ONLY = os.environ.get("SIMULATION_ONLY", "False").lower() == "true" + SIMULATION_MODE = False # Trading hours MARKET_OPEN_TIME = os.environ.get("MARKET_OPEN_TIME", "09:15") diff --git a/src/position_manager.py b/src/position_manager.py index c3131a4..0e1c2b5 100644 --- a/src/position_manager.py +++ b/src/position_manager.py @@ -1,33 +1,42 @@ import os import json from logger_config import get_logger -from trade_constants import ORDER_DIRECTION_BUY, ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET +from trade_constants import ( + ORDER_DIRECTION_BUY, + ORDER_TYPE_LIMIT, + ORDER_TYPE_MARKET, + ORDER_STATUS_COMPLETED, + ORDER_STATUS_CANCELLED, + ORDER_STATUS_FAILED, +) from local_position import LocalPosition from local_order import LocalOrder from t0_stocks import is_t0 +from typing import Dict # 获取日志记录器 -logger = get_logger('position_manager') +logger = get_logger("position_manager") -class PositionManager(): + +class PositionManager: """实盘策略持仓管理器,负责管理不同策略在实盘环境下的持仓情况""" - + def __init__(self, strategy_name="default_strategy"): """初始化实盘持仓管理器""" super().__init__() self.strategy_name = strategy_name # 策略持仓信息 - self.positions = {} # {股票代码 -> LocalPosition} + self.positions: Dict[str, LocalPosition] = {} # {股票代码 -> LocalPosition} # 待处理订单信息 self.pending_orders = {} # {order_id -> LocalOrder} - self.data_path = self.strategy_name + '_positions.json' + self.data_path = self.strategy_name + "_positions.json" self.load_data() - + def update_position(self, code, direction, amount): # 如果股票代码在持仓字典中不存在,初始化它 if code not in self.positions: self.positions[code] = LocalPosition(code, 0, 0) - + # 根据方向更新持仓 position = self.positions[code] is_t0_stock = is_t0(code) @@ -38,80 +47,90 @@ class PositionManager(): else: # sell position.total_amount -= amount position.closeable_amount -= amount - - logger.info(f"更新策略持仓 - 策略: {self.strategy_name}, 代码: {code}, 方向: {direction}, 数量: {amount}, " - f"更新后总量: {position.total_amount}, " - f"可用: {position.closeable_amount}") - + + logger.info( + f"更新策略持仓 - 策略: {self.strategy_name}, 代码: {code}, 方向: {direction}, 数量: {amount}, " + f"更新后总量: {position.total_amount}, " + f"可用: {position.closeable_amount}" + ) + # 移除total_amount为0的持仓 if code in self.positions and self.positions[code].total_amount <= 0: del self.positions[code] logger.info(f"移除空持仓 - 策略: {self.strategy_name}, 代码: {code}") - - def add_pending_order(self, order_id, code, price, amount, direction, order_type=ORDER_TYPE_LIMIT): + + def add_pending_order( + self, order_id, code, price, amount, direction, order_type=ORDER_TYPE_LIMIT + ): if not self.strategy_name: return - + order = LocalOrder(order_id, code, price, amount, direction, order_type) self.pending_orders[order_id] = order - logger.info(f"添加订单 - ID: {order_id}, 策略: {self.strategy_name}, 代码: {code}, 方向: {direction}, " - f"数量: {amount}, 价格: {price}, 类型: {order_type}") - - def update_order_status(self, order_id, filled,new_status): + logger.info( + f"添加订单 - ID: {order_id}, 策略: {self.strategy_name}, 代码: {code}, 方向: {direction}, " + f"数量: {amount}, 价格: {price}, 类型: {order_type}" + ) + + def update_order_status(self, order_id, filled, new_status): if order_id in self.pending_orders: _order = self.pending_orders[order_id] # 记录之前的状态用于日志 previous_status = _order.status - + # 更新状态 _order.status = new_status _order.filled = filled - + # 记录状态变化日志 if previous_status != new_status: code = self.pending_orders[order_id].code - logger.info(f"订单状态变化: ID={order_id}, 代码={code}, 旧状态={previous_status}, 新状态={new_status}") + logger.info( + f"订单状态变化: ID={order_id}, 代码={code}, 旧状态={previous_status}, 新状态={new_status}" + ) # 如果订单已完成,移除它 - if new_status in ['completed', 'cancelled', 'failed']: + if new_status in [ + ORDER_STATUS_COMPLETED, + ORDER_STATUS_CANCELLED, + ORDER_STATUS_FAILED, + ]: # 保留订单信息以供参考,但标记为已完成 del self.pending_orders[order_id] logger.info(f"订单已删除 - ID: {order_id}, 状态: {new_status}") - + return True return False - - def get_pending_order(self, order_id): + + def get_pending_order(self, order_id) -> LocalOrder: """获取未完成委托信息 - + Args: order_id: 订单ID - + Returns: dict: 委托信息,如果不存在返回None """ return self.pending_orders.get(order_id) - + def get_pending_orders(self): """获取所有未完成委托 - + Returns: dict: 订单ID到委托信息的映射 """ return self.pending_orders - - def get_positions(self): + + def get_positions(self) -> Dict[str, LocalPosition]: """获取策略持仓 - - Args: - strategy_name: 策略名称,如果为None,返回所有持仓 - + Returns: - dict: 策略持仓信息 + Dict[str, LocalPosition]: + key为股票代码(str),value为LocalPosition对象,若无持仓则返回空字典。 """ return self.positions - + def save_data(self): """保存策略数据""" try: @@ -119,75 +138,84 @@ class PositionManager(): positions_dict = {} for code, pos in self.positions.items(): positions_dict[code] = { - 'code': pos.code, - 'total_amount': pos.total_amount, - 'closeable_amount': pos.closeable_amount + "code": pos.code, + "total_amount": pos.total_amount, + "closeable_amount": pos.closeable_amount, } - + pending_orders_dict = {} for order_id, order in self.pending_orders.items(): pending_orders_dict[order_id] = { - 'order_id': order.order_id, - 'code': order.code, - 'price': order.price, - 'amount': order.amount, - 'filled': order.filled, - 'direction': order.direction, - 'order_type': order.order_type, - 'status': order.status, - 'created_time': order.created_time.isoformat() if hasattr(order, 'created_time') else None + "order_id": order.order_id, + "code": order.code, + "price": order.price, + "amount": order.amount, + "filled": order.filled, + "direction": order.direction, + "order_type": order.order_type, + "status": order.status, + "created_time": ( + order.created_time.isoformat() + if hasattr(order, "created_time") + else None + ), } - - with open(self.data_path, 'w') as f: - json.dump({ - 'positions': positions_dict, - 'pending_orders': pending_orders_dict - }, f) + + with open(self.data_path, "w") as f: + json.dump( + { + "positions": positions_dict, + "pending_orders": pending_orders_dict, + }, + f, + ) logger.info("成功保存实盘策略数据") except Exception as e: logger.error(f"保存实盘策略数据失败: {str(e)}") - + def load_data(self): """加载策略数据""" try: if os.path.exists(self.data_path): from datetime import datetime - - with open(self.data_path, 'r') as f: + + with open(self.data_path, "r") as f: data = json.load(f) - + # 还原positions对象 self.positions = {} - positions_dict = data.get('positions', {}) + positions_dict = data.get("positions", {}) for code, pos_data in positions_dict.items(): self.positions[code] = LocalPosition( - pos_data['code'], - pos_data['total_amount'], - pos_data['closeable_amount'] + pos_data["code"], + pos_data["total_amount"], + pos_data["closeable_amount"], ) - + # 还原pending_orders对象 self.pending_orders = {} - pending_orders_dict = data.get('pending_orders', {}) + pending_orders_dict = data.get("pending_orders", {}) for order_id, order_data in pending_orders_dict.items(): order = LocalOrder( - order_data['order_id'], - order_data['code'], - order_data['price'], - order_data['amount'], - order_data['direction'], - order_data['order_type'] + order_data["order_id"], + order_data["code"], + order_data["price"], + order_data["amount"], + order_data["direction"], + order_data["order_type"], ) - order.filled = order_data['filled'] - order.status = order_data['status'] - if order_data.get('created_time'): + order.filled = order_data["filled"] + order.status = order_data["status"] + if order_data.get("created_time"): try: - order.created_time = datetime.fromisoformat(order_data['created_time']) + order.created_time = datetime.fromisoformat( + order_data["created_time"] + ) except (ValueError, TypeError): order.created_time = datetime.now() - + self.pending_orders[order_id] = order - + logger.info("已加载实盘策略数据") logger.info(f"策略数: {len(self.positions)}") else: @@ -199,9 +227,20 @@ class PositionManager(): # 初始化空数据结构 self.positions = {} self.pending_orders = {} - + def clear(self): """清除所有持仓管理数据""" self.positions = {} self.pending_orders = {} self.save_data() + + def update_closeable_amount(self): + """更新可卖持仓""" + for _, position in self.positions.items(): + if position.closeable_amount != position.total_amount: + position.closeable_amount = position.total_amount + + def clear_pending_orders(self): + """清除所有未完成订单""" + self.pending_orders = {} + self.save_data() diff --git a/src/real/real_trader_manager.py b/src/real/real_trader_manager.py index 830e604..080fbcc 100644 --- a/src/real/real_trader_manager.py +++ b/src/real/real_trader_manager.py @@ -5,58 +5,90 @@ from xtquant import xtconstant from logger_config import get_logger from config import Config import json +from typing import Dict +from position_manager import PositionManager +from functools import wraps +from trade_constants import ( + ORDER_STATUS_COMPLETED, + ORDER_STATUS_CANCELLED, + ORDER_STATUS_PENDING, + ORDER_STATUS_FAILED, + ORDER_STATUS_PARTIAL, + ORDER_DIRECTION_BUY, + ORDER_DIRECTION_SELL, + ORDER_TYPE_LIMIT, + ORDER_TYPE_MARKET, +) +from xt_trader import XtTrader # 获取日志记录器 -logger = get_logger('real_trader_manager') +logger = get_logger("real_trader_manager") + + +def run_threaded(func): + @wraps(func) + def wrapper(*args, **kwargs): + thread = threading.Thread(target=func, args=args, kwargs=kwargs) + thread.start() + + return wrapper + class RealTraderManager: """实盘交易管理器,处理实盘下单失败、部分成交等问题,尽量保证仓位与策略信号一致""" - - def __init__(self, trader, position_manager): + + def __init__(self, trader: XtTrader): """初始化实盘交易管理器 - + Args: trader: XtTrader实例 position_manager: StrategyPositionManager实例 """ # 使用传入的trader和position_manager实例 self.trader = trader - self.position_manager = position_manager - - # 确保已登录 - if not self.trader.is_logged_in(): - self.trader.login() - + # 启动调度器 self._start_scheduler() - + logger.info("实盘交易管理器初始化完成") - + def _start_scheduler(self): - """启动定时任务调度器""" - # 每分钟检查一次未完成订单状态并处理 - schedule.every(1).minutes.do(self.check_pending_orders) - - # 每天收盘后清理过期未完成订单 - schedule.every().day.at(Config.STRATEGY_SAVE_TIME).do(self.clean_expired_orders) - - # 启动调度线程 + # 每日定时清理(增加配置校验) + if hasattr(Config, "STRATEGY_SAVE_TIME"): + try: + schedule.every().day.at(Config.STRATEGY_SAVE_TIME).do( + run_threaded(self.clean_expired_orders) + ) + schedule.every().day.at(Config.STRATEGY_SAVE_TIME).do( + run_threaded(self.update_closeable_amount) + ) + except Exception as e: + logger.error(f"清理任务配置错误: {e}") + else: + logger.error("STRATEGY_SAVE_TIME 未配置") + + # 启动高精度调度线程 def run_scheduler(): while True: try: schedule.run_pending() - time.sleep(10) + time.sleep(1) # 将休眠时间缩短至1秒提高精度 except Exception as e: - logger.error(f"调度器运行错误: {str(e)}") - - scheduler_thread = threading.Thread(target=run_scheduler) - scheduler_thread.daemon = True + logger.error(f"调度器异常: {e}", exc_info=True) + time.sleep(10) # 发生错误时延长休眠避免日志风暴 + + scheduler_thread = threading.Thread( + target=run_scheduler, name="SchedulerThread" + ) + scheduler_thread.daemon = True # 设为守护线程随主进程退出 scheduler_thread.start() logger.info("交易管理器调度器已启动") - - def place_order(self, strategy_name, code, direction, amount, price, order_type='limit'): + + def place_order( + self, strategy_name, code, direction, amount, price, order_type=ORDER_TYPE_LIMIT + ): """下单接口,处理买入/卖出请求 - + Args: strategy_name: 策略名称 code: 股票代码 @@ -64,366 +96,318 @@ class RealTraderManager: amount: 交易数量 price: 交易价格(市价单时可为0) order_type: 订单类型,'limit'表示限价单,'market'表示市价单,默认为'limit' - + Returns: dict: 包含订单ID和状态信息 """ if not strategy_name or not code or not direction: logger.error("下单参数不完整") return {"success": False, "error": "参数不完整"} - + # 检查交易方向 - if direction not in ['buy', 'sell']: + if direction not in [ORDER_DIRECTION_BUY, ORDER_DIRECTION_SELL]: logger.error(f"无效的交易方向: {direction}") return {"success": False, "error": "无效的交易方向"} - + # 检查订单类型 - if order_type not in ['limit', 'market']: + if order_type not in [ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET]: logger.error(f"无效的订单类型: {order_type}") - return {"success": False, "error": "无效的订单类型,必须是'limit'或'market'"} - + return { + "success": False, + "error": "无效的订单类型,必须是'limit'或'market'", + } + try: # 对于限价单,检查资金和持仓是否足够 - if order_type == 'limit' and not self._check_order_feasibility(code, direction, amount, price): - logger.warning(f"资金或持仓不足,忽略订单: {direction} {code} {amount}股 {price}元") + if order_type == ORDER_TYPE_LIMIT and not self._check_order_feasibility( + code, direction, amount, price + ): + logger.warning( + f"资金或持仓不足,忽略订单: {direction} {code} {amount}股 {price}元" + ) return {"success": False, "error": "资金或持仓不足"} - + # 下单 - logger.info(f"准备{direction}订单: 代码={code}, 数量={amount}, 价格={price}, 订单类型={order_type}") - if direction == 'buy': + logger.info( + f"准备{direction}订单: 代码={code}, 数量={amount}, 价格={price}, 订单类型={order_type}" + ) + if direction == ORDER_DIRECTION_BUY: result = self.trader.buy(code, price, amount, order_type) else: result = self.trader.sell(code, price, amount, order_type) - - order_id = result.get('order_id') - if not order_id or order_id == 'simulation': + + order_id = result.get("order_id") + if not order_id: logger.error(f"下单失败: {result}") return {"success": False, "error": "下单失败"} - + # 添加未完成委托到position_manager - self.position_manager.add_pending_order( - order_id, - strategy_name, - code, - price, - amount, - direction, - order_type + position_manager = self.trader.get_position_manager(strategy_name) + position_manager.add_pending_order(order_id, code, price, amount, direction, order_type) + + logger.info( + f"已提交订单: ID={order_id}, 策略={strategy_name}, 代码={code}, 方向={direction}, 数量={amount}, 价格={price}, 类型={order_type}" ) - - logger.info(f"已提交订单: ID={order_id}, 策略={strategy_name}, 代码={code}, 方向={direction}, 数量={amount}, 价格={price}, 类型={order_type}") - + # 立即更新一次订单状态 - self._update_order_status(order_id) - + self.check_and_retry(order_id, strategy_name, code, direction, amount, 1) + return {"success": True, "order_id": order_id} - + except Exception as e: logger.error(f"下单过程发生异常: {str(e)}") return {"success": False, "error": f"下单异常: {str(e)}"} - - def check_pending_orders(self): - """检查所有未完成订单状态并处理,定时任务调用""" - try: - logger.info("开始检查未完成订单...") - - # 获取所有未完成订单 - pending_orders = self.position_manager.get_pending_orders() - - # 如果没有未完成订单,直接返回 - if not pending_orders: - logger.info("没有未完成订单需要检查") - return - - # 获取最新的委托列表 - try: - entrusts = self.trader.get_today_orders() - if entrusts is None: - logger.error("获取今日委托失败,跳过本次检查") - return - - entrust_map = {str(e['order_id']): e for e in entrusts} - except Exception as e: - logger.error(f"获取今日委托失败: {str(e)},跳过本次检查") - return - - # 检查每个未完成订单 - for order_id, order_info in list(pending_orders.items()): - try: - # 跳过已完成的订单 - if order_info['status'] in ['completed', 'cancelled', 'failed']: - continue - - # 更新订单状态 - self._update_order_status(order_id, entrust_map) - - # 获取最新的订单信息 - order_info = self.position_manager.get_pending_order(order_id) - if not order_info: - continue - - # 处理未成交或部分成交的订单 - current_time = time.time() - order_age = current_time - order_info['created_time'] - - # 如果订单超过配置的超时时间且状态仍为pending或partial - if order_age > Config.RTM_ORDER_TIMEOUT and order_info['status'] in ['pending', 'partial']: - # 记录超时信息 - logger.warning(f"订单已超时({order_age:.0f}秒 > {Config.RTM_ORDER_TIMEOUT}秒): ID={order_id}, 代码={order_info['code']}, 状态={order_info['status']}") - - # 如果是部分成交,记录详情 - if order_info['status'] == 'partial' and 'traded_volume' in order_info: - original = order_info['target_amount'] - traded = order_info['traded_volume'] - remaining = original - traded - logger.info(f"订单部分成交详情: ID={order_id}, 原始数量={original}, 已成交={traded}, 剩余={remaining}") - - self._handle_timeout_order(order_id, order_info) - except Exception as e: - logger.error(f"处理订单 {order_id} 时出错: {str(e)}") - - logger.info("未完成订单检查完毕") - - except Exception as e: - logger.error(f"检查未完成订单时发生异常: {str(e)}") - - def _update_order_status(self, order_id, entrust_map=None): - """更新单个订单状态 + def _place_market_order_for_remainder(self, strategy_name, code, direction, left_amount): + """对未完成的订单进行补单,下市价单 + Args: - order_id: 订单ID - entrust_map: 可选的委托字典,如果为None则重新获取 + strategy_name: 策略名称 + code: 股票代码 + direction: 交易方向 + left_amount: 剩余数量 + available_retry_count: 重试次数 + + Returns: + bool: 补单是否成功 """ - # 检查订单是否存在 - order_info = self.position_manager.get_pending_order(order_id) + if left_amount <= 0: + logger.info(f"无需补单,剩余数量为零或负数: {left_amount}") + return True + + logger.info(f"限价单补单: 市价单, 剩余数量={left_amount}") + new_order = self.place_order(strategy_name, code, direction, left_amount, 0, ORDER_TYPE_MARKET) + new_order_id = new_order.get("order_id") + if new_order.get("success") and new_order_id: + # 立即检查新市价单 + self.check_and_retry(new_order_id, strategy_name, code, direction, left_amount) + return True + else: + logger.error(f"补单失败: {new_order}") + return False + + def check_and_retry(self, order_id, strategy_name, code, direction, amount, available_retry_count=1): + position_manager = self.trader.get_position_manager(strategy_name) + order_info = position_manager.get_pending_order(order_id) + filled = order_info.filled + target_amount = order_info.amount + if not order_info: + logger.warning(f"订单信息不存在: ID={order_id}") return - try: - # 如果没有提供委托字典,则获取当前委托 - if entrust_map is None: - entrusts = self.trader.get_today_orders() - entrust_map = {str(e['order_id']): e for e in entrusts} - - # 查找对应的委托记录 - entrust = entrust_map.get(str(order_id)) - - if entrust: - # 获取订单之前的状态,用于判断是否发生变化 - previous_status = order_info.get('status') - previous_volume = order_info.get('traded_volume', 0) - - # 根据委托状态更新订单状态 - if entrust['order_status'] == xtconstant.ORDER_SUCCEEDED: - # 全部成交 - self.position_manager.update_order_status(order_id, 'completed') - # 更新持仓 - self.position_manager.update_position( - order_info['strategy_name'], - order_info['code'], - order_info['direction'], - order_info['target_amount'] - ) - - elif entrust['order_status'] == xtconstant.ORDER_PART_SUCC: - # 部分成交 - current_volume = entrust.get('traded_volume', 0) - self.position_manager.update_order_status( - order_id, - 'partial', - traded_volume=current_volume - ) - - # 如果成交量有变化,记录日志并更新持仓 - if current_volume != previous_volume: - target_amount = order_info['target_amount'] - logger.info(f"订单部分成交更新: ID={order_id}, 代码={entrust['stock_code']}, 目标数量={target_amount}, 已成交数量={current_volume}, 剩余数量={target_amount - current_volume}") - - # 更新持仓(仅更新已成交部分) - if current_volume > 0: - self.position_manager.update_position( - order_info['strategy_name'], - order_info['code'], - order_info['direction'], - current_volume - ) - - elif entrust['order_status'] in [xtconstant.ORDER_CANCELED, xtconstant.ORDER_JUNK]: - # 已撤单或废单 - self.position_manager.update_order_status( - order_id, - 'cancelled', - err_msg=entrust.get('err_msg', '未知原因') - ) - - elif entrust['order_status'] in [xtconstant.ORDER_UNREPORTED, xtconstant.ORDER_WAIT_REPORTING, xtconstant.ORDER_REPORTED]: - # 未报、待报、已报 - if previous_status != 'pending': - self.position_manager.update_order_status(order_id, 'pending') - else: - # 委托列表中找不到该订单,可能已经太久 - current_time = time.time() - if current_time - order_info['created_time'] > 24 * 60 * 60: - previous_status = order_info.get('status') - self.position_manager.update_order_status(order_id, 'failed') - logger.warning(f"订单状态未知且过期: ID={order_id}, 旧状态={previous_status}, 新状态=failed, 创建时长={(current_time - order_info['created_time'])/3600:.1f}小时") - - except Exception as e: - logger.error(f"更新订单状态时发生异常: order_id={order_id}, error={str(e)}") - - def _handle_timeout_order(self, order_id, order_info): - """处理超时或部分成交的订单 + order_type = order_info.order_type + status = self._update_order_status(order_id, strategy_name) + if order_type == ORDER_TYPE_MARKET: + # 市价单,只递归检查 + if status in [ORDER_STATUS_PENDING, ORDER_STATUS_PARTIAL]: + logger.info(f"市价单未完成,1分钟后继续检查: ID={order_id}, 状态={status}") + threading.Timer(60, self.check_and_retry, args=(order_id, strategy_name, code, direction, amount)).start() + else: + logger.info(f"市价单已完成: ID={order_id}, 状态={status}") + elif order_type == ORDER_TYPE_LIMIT: + # 限价单,未完成则撤单补市价单 + if status in [ORDER_STATUS_PENDING, ORDER_STATUS_PARTIAL]: + if available_retry_count > 0: + logger.info(f"限价单未完成,1分钟后继续检查: ID={order_id}, 状态={status}") + threading.Timer(60, self.check_and_retry, args=(order_id, strategy_name, code, direction, amount, 0)).start() + else: + # 尝试撤单 + try: + logger.info(f"限价单未完成,尝试撤单: ID={order_id}, 状态={status}") + self.trader.cancel(order_id) + position_manager.update_order_status(order_id, 0, ORDER_STATUS_CANCELLED) + except Exception as e: + logger.error(f"撤单失败: order_id={order_id}, error={str(e)}") + + # 计算剩余数量, 如果剩余数量大于0, 则补单 + left_amount = target_amount - filled + self._place_market_order_for_remainder(strategy_name, code, direction, left_amount) + else: + logger.info(f"限价单已完成: ID={order_id}, 状态={status}") + else: + logger.warning(f"未知订单类型: ID={order_id}, type={order_type}") + + def _update_order_status(self, order_id, strategy_name): + """更新单个订单状态 + Args: order_id: 订单ID - order_info: 订单信息字典 """ + # 检查订单是否存在 + position_manager = self.trader.get_position_manager(strategy_name) + order_info = position_manager.get_pending_order(order_id) + if not order_info: + return None + try: - # 首先尝试撤单 - logger.info(f"尝试撤销超时订单: ID={order_id}, 代码={order_info['code']}, 超时时间={(time.time() - order_info['created_time']):.0f}秒") - cancel_result = self.trader.cancel(order_id) - - # 记录撤单结果 - if isinstance(cancel_result, dict): - result_str = json.dumps(cancel_result) - else: - result_str = str(cancel_result) - logger.info(f"撤单结果: ID={order_id}, 结果={result_str}") - - # 计算未成交数量 - original_amount = order_info['target_amount'] - traded_amount = order_info.get('traded_volume', 0) - remaining_amount = original_amount - traded_amount - - # 记录详细的成交情况 - logger.info(f"订单成交情况: ID={order_id}, 代码={order_info['code']}, 原始数量={original_amount}, 已成交={traded_amount}, 剩余={remaining_amount}") - - # 如果有未成交的部分,使用市价单补充交易 - if remaining_amount > 0: - # 递增重试计数 - new_retry_count = self.position_manager.increment_retry_count(order_id) - - # 使用市价单进行补单 - new_order_type = 'market' - new_price = 0 # 市价单价格设为0 - - logger.info(f"准备补充交易: 代码={order_info['code']}, 方向={order_info['direction']}, 补充数量={remaining_amount}, 重试次数={new_retry_count}/{Config.RTM_MAX_RETRIES}") - - # 如果重试次数少于最大重试次数,则进行补单 - if new_retry_count <= Config.RTM_MAX_RETRIES: - # 下新订单 - new_order = self.place_order( - order_info['strategy_name'], - order_info['code'], - order_info['direction'], - remaining_amount, - new_price, - new_order_type + # 获取订单之前的状态,用于判断是否发生变化 + previous_status = order_info.status + previous_volume = order_info.filled + + updated_order = self.trader.get_order(order_id) + + # 根据委托状态更新订单状态 + if updated_order["order_status"] == xtconstant.ORDER_SUCCEEDED: + # 全部成交 + filled = updated_order["traded_volume"] + position_manager.update_order_status(order_id, filled, ORDER_STATUS_COMPLETED) + # 更新持仓 + position_manager.update_position( + order_info.code, + order_info.direction, + filled, + ) + return ORDER_STATUS_COMPLETED + elif updated_order["order_status"] == xtconstant.ORDER_PART_SUCC: + # 部分成交 + filled = updated_order.get("traded_volume", 0) + position_manager.update_order_status( + order_id, filled, ORDER_STATUS_PARTIAL + ) + + # 如果成交量有变化,记录日志并更新持仓 + if filled != previous_volume: + target_amount = order_info.amount + logger.info( + f"订单部分成交更新: ID={order_id}, 代码={order_info.code}, 目标数量={target_amount}, 已成交数量={filled}, 剩余数量={target_amount - filled}" ) - - if new_order.get('success'): - logger.info(f"补单成功: 原订单ID={order_id}, 新订单ID={new_order['order_id']}, 代码={order_info['code']}, 方向={order_info['direction']}, 数量={remaining_amount}, 订单类型={new_order_type}") - else: - logger.error(f"补单失败: 原订单ID={order_id}, 错误={new_order.get('error')}, 代码={order_info['code']}, 方向={order_info['direction']}, 数量={remaining_amount}, 订单类型={new_order_type}") - else: - logger.warning(f"订单重试次数过多,不再尝试: ID={order_id}, 重试次数={new_retry_count}/{Config.RTM_MAX_RETRIES}, 代码={order_info['code']}, 方向={order_info['direction']}, 未成交数量={remaining_amount}") - else: - logger.info(f"订单已全部成交,无需补单: ID={order_id}, 代码={order_info['code']}, 成交数量={traded_amount}") - - # 更新原订单状态 - previous_status = order_info['status'] - self.position_manager.update_order_status(order_id, 'cancelled') - logger.info(f"更新原订单状态: ID={order_id}, 旧状态={previous_status}, 新状态=cancelled") - + + # 更新持仓(仅更新已成交部分) + if filled > 0: + position_manager.update_position( + order_info.code, + order_info.direction, + filled, + ) + return ORDER_STATUS_PARTIAL + elif updated_order["order_status"] in [ + xtconstant.ORDER_CANCELED, + xtconstant.ORDER_JUNK, + ]: + # 已撤单或废单 + position_manager.update_order_status( + order_id, + 0, + ORDER_STATUS_CANCELLED + ) + return ORDER_STATUS_CANCELLED + + elif updated_order["order_status"] in [ + xtconstant.ORDER_UNREPORTED, + xtconstant.ORDER_WAIT_REPORTING, + xtconstant.ORDER_REPORTED, + ]: + # 未报、待报、已报 + if previous_status != ORDER_STATUS_PENDING: + position_manager.update_order_status(order_id, 0, ORDER_STATUS_PENDING) + return ORDER_STATUS_PENDING + except Exception as e: - logger.error(f"处理超时订单时发生异常: order_id={order_id}, error={str(e)}") - + logger.error(f"更新订单状态时发生异常: order_id={order_id}, error={str(e)}") + return None + def _check_order_feasibility(self, code, direction, amount, price): """检查订单是否可行(资金或持仓是否足够) - + Args: code: 股票代码 direction: 交易方向 amount: 交易数量 price: 交易价格 - + Returns: bool: 订单是否可行 """ try: - if direction == 'buy': + if direction == ORDER_DIRECTION_BUY: # 检查资金是否足够 balance = self.trader.get_balance() if not balance: logger.error("获取账户余额失败") return False - + # 计算所需资金(加上3%的手续费作为缓冲) required_cash = price * amount * 1.03 - available_cash = balance.get('cash', 0) - + available_cash = balance.get("cash", 0) - balance.get("frozen_cash", 0) + if required_cash > available_cash: - logger.warning(f"资金不足: 需要 {required_cash:.2f}, 可用 {available_cash:.2f}") + logger.warning( + f"资金不足: 需要 {required_cash:.2f}, 可用 {available_cash:.2f}" + ) return False - + return True - - elif direction == 'sell': + + elif direction == "sell": # 检查持仓是否足够 - positions = self.trader.get_positions() - position = next((p for p in positions if p.get('stock_code') == code), None) - + position = self.trader.get_position(code) + if not position: logger.warning(f"没有持仓: {code}") return False - - available_volume = position.get('can_use_volume', 0) - + + available_volume = position.get("can_use_volume", 0) + if amount > available_volume: - logger.warning(f"可用持仓不足: 需要 {amount}, 可用 {available_volume}") + logger.warning( + f"可用持仓不足: 需要 {amount}, 可用 {available_volume}" + ) return False - + return True - + return False - + except Exception as e: logger.error(f"检查订单可行性时发生异常: {str(e)}") return False - + def clean_expired_orders(self): """清理过期的未完成订单""" try: logger.info("开始清理过期未完成订单...") - + # 获取所有未完成订单 - pending_orders = self.position_manager.get_pending_orders() - - if not pending_orders: - logger.info("没有未完成订单需要清理") - return - - # 遍历未完成订单,检查是否有无法成交的订单(如跌停无法卖出) - for order_id, order_info in list(pending_orders.items()): - try: - # 只处理pending和partial状态的订单 - if order_info['status'] not in ['pending', 'partial']: - continue - - # 标记为失败并记录日志 - self.position_manager.update_order_status(order_id, 'failed', err_msg="收盘清理:订单无法成交") - - logger.warning(f"清理无法成交订单: ID={order_id}, 代码={order_info['code']}, 方向={order_info['direction']}, " - f"数量={order_info['target_amount']}, 已成交数量={order_info.get('traded_volume', 0)}") - - # 对于特殊情况(如跌停无法卖出),记录错误日志 - if order_info['direction'] == 'sell': - logger.error(f"可能存在跌停无法卖出情况: ID={order_id}, 代码={order_info['code']}, " - f"目标数量={order_info['target_amount']}, 已成交数量={order_info.get('traded_volume', 0)}") - except Exception as e: - logger.error(f"清理订单 {order_id} 时出错: {str(e)}") - + position_managers = self.trader.get_all_position_managers() + + # 遍历所有持仓管理器 + for position_manager in position_managers.values(): + # 获取所有未完成订单 + pending_orders = position_manager.get_pending_orders() + + # 遍历未完成订单,检查是否有无法成交的订单(如跌停无法卖出) + for order_id, order_info in pending_orders.items(): + try: + logger.warning( + f"清理无法成交订单: ID={order_id}, 代码={order_info.code}, 方向={order_info.direction}, " + f"数量={order_info.amount}, 已成交数量={order_info.filled}" + ) + + except Exception as e: + logger.error(f"清理订单 {order_id} 时出错: {str(e)}") + + position_manager.clear_pending_orders() + logger.info("过期未完成订单清理完毕") - + except Exception as e: - logger.error(f"清理过期未完成订单时发生异常: {str(e)}") \ No newline at end of file + logger.error(f"清理过期未完成订单时发生异常: {str(e)}") + + def update_closeable_amount(self): + """更新可卖持仓""" + try: + logger.info("开始更新可卖持仓...") + + # 获取所有持仓 + position_managers = self.trader.get_all_position_managers() + + # 遍历持仓,更新可卖持仓 + for position_manager in position_managers.values(): + position_manager.update_closeable_amount() + + logger.info("可卖持仓更新完毕") + + except Exception as e: + logger.error(f"更新可卖持仓时发生异常: {str(e)}") diff --git a/src/real/xt_trader.py b/src/real/xt_trader.py index 2feb7ab..61a6dbc 100644 --- a/src/real/xt_trader.py +++ b/src/real/xt_trader.py @@ -248,7 +248,7 @@ class XtTrader(BaseTrader): def cancel(self, order_id): # 撤单接口需要订单编号 result = self.xt_trader.cancel_order_stock(self.account, int(order_id)) - return {"result": result == 0, "message": f"撤单结果: {result}"} + return {"success": result == 0, "message": f"撤单结果: {result}"} def get_quote(self, code): """获取行情数据 diff --git a/src/simulation/simulation_trader.py b/src/simulation/simulation_trader.py index a066396..7987d42 100644 --- a/src/simulation/simulation_trader.py +++ b/src/simulation/simulation_trader.py @@ -1,16 +1,25 @@ from logger_config import get_logger -from trade_constants import TRADE_TYPE_SIMULATION, ORDER_DIRECTION_BUY, ORDER_DIRECTION_SELL +from trade_constants import ( + TRADE_TYPE_SIMULATION, + ORDER_DIRECTION_BUY, + ORDER_DIRECTION_SELL, + ORDER_STATUS_COMPLETED, + ORDER_STATUS_CANCELLED, +) from position_manager import PositionManager +from base_trader import BaseTrader +import random -class SimulationTrader: + +class SimulationTrader(BaseTrader): def __init__(self, logger=None): - self.logger = logger or get_logger('simulation_trader') + self.logger = logger or get_logger("simulation_trader") # 模拟资金账户信息 self.sim_balance = {"cash": 1000000.00, "frozen": 0.00, "total": 1000000.00} - self.position_manager = PositionManager(TRADE_TYPE_SIMULATION) + def is_logged_in(self): """检查交易系统是否已经登录 - + Returns: bool: True表示已登录,模拟交易系统总是返回已登录状态 """ @@ -24,102 +33,135 @@ class SimulationTrader: self.logger.info("模拟交易:登出成功") return True - def buy(self, code, price, amount, strategy_name = "default_strategy"): + def buy(self, code, price, amount, strategy_name="default_strategy"): message = f"模拟买入 - 代码: {code}, 价格: {price}, 数量: {amount}, 策略: {strategy_name}" self.logger.info(message) - + # 计算交易成本 cost = price * amount - + # 检查余额是否足够 if self.sim_balance["cash"] < cost: message = f"模拟买入失败 - 代码: {code}, 资金不足" self.logger.warning(message) return {"order_id": None, "message": message, "success": False} - + # 更新资金 self.sim_balance["cash"] -= cost - + # 更新持仓管理器 - self.position_manager.update_position(strategy_name, code, ORDER_DIRECTION_BUY, amount) - + position_manager = self.get_position_manager(strategy_name) + position_manager.update_position( + strategy_name, code, ORDER_DIRECTION_BUY, amount + ) + order_id = random.randint(1, 999999) # 使用随机函数生成小于1000000的随机整数 + position_manager.add_pending_order( + order_id, code, price, amount, ORDER_DIRECTION_BUY + ) + # 假设立刻全部成交 + position_manager.update_order_status(order_id, amount, ORDER_STATUS_COMPLETED) + # 更新总资产 self._update_total_assets() - - return {"order_id": "simulation", "message": message, "success": True} - def sell(self, code, price, amount, strategy_name = "default_strategy"): + return {"order_id": order_id, "message": message, "success": True} + + def sell(self, code, price, amount, strategy_name="default_strategy"): message = f"模拟卖出 - 代码: {code}, 价格: {price}, 数量: {amount}, 策略: {strategy_name}" self.logger.info(message) - + # 获取策略持仓 strategy_positions = self.position_manager.get_positions(strategy_name) - + # 检查持仓是否足够 - if code not in strategy_positions or strategy_positions[code]['closeable_amount'] < amount: + if ( + code not in strategy_positions + or strategy_positions[code]["closeable_amount"] < amount + ): message = f"模拟卖出失败 - 代码: {code}, 可用数量不足" self.logger.warning(message) return {"order_id": None, "message": message, "success": False} - + # 更新资金 proceeds = price * amount self.sim_balance["cash"] += proceeds - + # 更新持仓管理器 - self.position_manager.update_position(strategy_name, code, ORDER_DIRECTION_SELL, amount) - + position_manager = self.get_position_manager(strategy_name) + position_manager.update_position( + strategy_name, code, ORDER_DIRECTION_SELL, amount + ) + order_id = random.randint(1, 999999) # 使用随机函数生成小于1000000的随机整数 + position_manager.add_pending_order( + order_id, code, price, amount, ORDER_DIRECTION_SELL + ) + # 假设立刻全部成交 + position_manager.update_order_status(order_id, amount, ORDER_STATUS_COMPLETED) + # 更新总资产 self._update_total_assets() - - return {"order_id": "simulation", "message": message, "success": True} + + return {"order_id": order_id, "message": message, "success": True} def _update_total_assets(self): """更新总资产""" # 此处简化处理,在实际情况中应该计算所有持仓的市值 self.sim_balance["total"] = self.sim_balance["cash"] - - def cancel(self, entrust_no): - message = f"模拟撤单 - 委托号: {entrust_no}" + + def cancel(self, order_id, strategy_name="default_strategy"): + message = f"模拟撤单 - 委托号: {order_id}" self.logger.info(message) - return {"order_id": "simulation", "message": message} + position_manager = self.get_position_manager(strategy_name) + + if order_id in position_manager.pending_orders: + position_manager.update_order_status(order_id, 0, ORDER_STATUS_CANCELLED) + return {"order_id": "order_id", "message": message, "success": True} + else: + return {"order_id": None, "message": "订单不存在", "success": False} def get_balance(self): message = "模拟交易:查询余额" self.logger.info(message) return self.sim_balance - def get_positions(self): + def get_positions(self, strategy_name="default_strategy"): message = "模拟交易:查询持仓" self.logger.info(message) - - # 从持仓管理器获取所有策略的持仓 - all_positions = [] - for strategy_name, positions in self.position_manager.get_positions().items(): - for code, position_info in positions.items(): - all_positions.append({ - "account_id": "simulation", - "stock_code": code, - "strategy_name": strategy_name, - "volume": position_info["total_amount"], - "can_use_volume": position_info["closeable_amount"], - "open_price": 0.0, # 持仓管理器中没有记录价格信息 - "avg_price": 0.0, # 持仓管理器中没有记录价格信息 - "market_value": 0.0, # 持仓管理器中没有记录价格信息 - "frozen_volume": 0, - "on_road_volume": 0 - }) - - return all_positions + + position_manager = self.get_position_manager(strategy_name) + postions = position_manager.get_positions() + # convert to json list + return [position.to_dict() for position in postions.values()] def get_today_trades(self): message = "模拟交易:查询今日成交" self.logger.info(message) - return [] + return {"message": "模拟交易:查询今日成交未实现", "success": True} def get_today_orders(self): message = "模拟交易:查询今日委托" self.logger.info(message) - return [] + return {"message": "模拟交易:查询今日委托未实现", "success": True} def is_trading_time(self): - return True \ No newline at end of file + return True + + def get_position(self, stock_code, strategy_name="default_strategy"): + """查询指定股票代码的持仓信息 + Args: + stock_code: 股票代码,例如 "600000.SH" + strategy_name: 策略名称,默认为"default_strategy" + Returns: + dict: 持仓详情,如果未持有则返回None + """ + positions = self.position_manager.get_positions(strategy_name) + if stock_code in positions: + position_info = positions[stock_code] + return { + "account_id": "simulation", + "stock_code": stock_code, + "strategy_name": strategy_name, + "volume": position_info.total_amount, + "can_use_volume": position_info.closeable_amount, + } + return None diff --git a/src/trade_constants.py b/src/trade_constants.py index cda9257..a4da054 100644 --- a/src/trade_constants.py +++ b/src/trade_constants.py @@ -4,6 +4,7 @@ TRADE_TYPE_SIMULATION = 'simulation' # 订单状态 ORDER_STATUS_PENDING = 'pending' +ORDER_STATUS_PARTIAL = 'partial' ORDER_STATUS_COMPLETED = 'completed' ORDER_STATUS_CANCELLED = 'cancelled' ORDER_STATUS_FAILED = 'failed' diff --git a/src/trade_server.py b/src/trade_server.py index 79b72bc..a9fb77f 100644 --- a/src/trade_server.py +++ b/src/trade_server.py @@ -9,157 +9,50 @@ import concurrent.futures import atexit from simulation.simulation_trader import SimulationTrader import datetime -from strategy_position_manager import StrategyPositionManager from logger_config import get_logger +from real.real_trader_manager import RealTraderManager +from trade_constants import * # 获取日志记录器 -logger = get_logger('server') +logger = get_logger("server") # 全局交易实例(采用单例模式) -_sim_trader_instance = None # 模拟交易实例(单例) -_real_trader_instance = None # 实盘交易实例(单例) +_trader_instance = None # 交易实例(单例) _real_trader_manager_instance = None # 实盘交易管理器实例(单例) -# 添加线程锁,保护单例实例的创建 -_instance_lock = threading.RLock() - # 后台任务执行线程 _scheduler_thread = None -# 获取模拟交易实例的辅助函数 -def get_sim_trader(): - """获取模拟交易实例 - 保证单例模式 - - Returns: - 返回模拟交易单例实例 - """ - global _sim_trader_instance - with _instance_lock: - if _sim_trader_instance is None: - _sim_trader_instance = SimulationTrader() - return _sim_trader_instance - -# 获取实盘交易实例的辅助函数 -def get_real_trader(): - """获取实盘交易实例 - 保证单例模式 - - Returns: - 返回实盘交易单例实例 - """ - global _real_trader_instance - with _instance_lock: - if _real_trader_instance is None: - _real_trader_instance = XtTrader() - # 检查交易实例是否已登录,如果未登录则进行登录 - if not _real_trader_instance.is_logged_in(): - logger.info("创建新的XtTrader实例并登录") - login_success = _real_trader_instance.login() - if not login_success: - logger.error("XtTrader登录失败") - return _real_trader_instance # 获取实盘交易管理器实例的辅助函数 def get_real_trader_manager(): - """获取实盘交易管理器实例 - 保证单例模式 - - Returns: - 返回实盘交易管理器单例实例 - """ global _real_trader_manager_instance - with _instance_lock: - if _real_trader_manager_instance is None: - # 延迟导入避免循环依赖 - from real.real_trader_manager import RealTraderManager - _real_trader_manager_instance = RealTraderManager(get_real_trader()) - logger.info("创建新的RealTraderManager实例") + + if _real_trader_manager_instance is None: + _real_trader_manager_instance = ( + None if Config.SIMULATION_MODE else RealTraderManager(get_trader()) + ) + return _real_trader_manager_instance -# 判断当前是否应该使用模拟交易 -def should_use_simulation(): - """判断是否应该使用模拟交易 - - Returns: - tuple: (should_simulate: bool, simulation_reason: str) - should_simulate: 是否应该使用模拟交易 - simulation_reason: 使用模拟交易的原因 - """ - # 如果配置为仅模拟交易,返回True - if Config.SIMULATION_ONLY: - return True, "配置为仅模拟交易" - - # 判断当前是否为交易日(只基于日期,不考虑时间) - now = datetime.datetime.now() - - # 使用chinese_calendar判断是否为交易日 - from chinese_calendar import is_workday, is_holiday - is_trading_day = is_workday(now) and not is_holiday(now) - logger.debug(f"使用chinese_calendar判断交易日: {now.date()}, 是交易日: {is_trading_day}") - - # 如果不是交易日,返回True(使用模拟交易) - if not is_trading_day: - return True, f"当前非交易日 - {now.date()}" - - # 如果是交易日,无论是否在交易时间,都返回False(使用实盘) - return False, "" - -# 判断当前是否在交易时间内 -def is_trading_hours(): - """判断当前是否在交易时间内 - - Returns: - tuple: (is_trading: bool, message: str) - is_trading: 是否在交易时间 - message: 相关信息 - """ - 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) - - if is_trading_hour: - return True, "" - else: - return False, f"当前非交易时段 - 时间: {current_time.strftime('%H:%M:%S')}" # 获取交易实例 - 根据情况返回模拟或实盘交易实例 def get_trader(): - """获取交易实例 - 根据当前状态决定返回模拟还是实盘交易实例 - - Returns: - 返回交易实例,根据配置和当前时间决定是模拟交易还是实盘交易 - """ - should_simulate, _ = should_use_simulation() - if should_simulate: - return get_sim_trader() - else: - return get_real_trader() + global _trader_instance + + if _trader_instance is None: + _trader_instance = SimulationTrader() if Config.SIMULATION_MODE else XtTrader() + + return _trader_instance + + +def is_real_mode(): + return not Config.SIMULATION_MODE -# 获取指定类型的交易实例 - 供内部API查询等使用 -def get_trader_by_type(trader_type='auto'): - """获取指定类型的交易实例 - - Args: - trader_type: 'simulation'=模拟交易, 'real'=实盘交易, 'auto'=自动判断 - - Returns: - 指定类型的交易实例 - """ - if trader_type == 'simulation': - return get_sim_trader() - elif trader_type == 'real': - return get_real_trader() - else: # 'auto' - return get_trader() def run_daily(time_str, job_func): """设置每天在指定时间运行的任务 - + Args: time_str: 运行时间,格式为"HH:MM" job_func: 要运行的函数 @@ -187,61 +80,12 @@ _scheduler_thread_running = True _scheduler_thread = threading.Thread(target=run_pending_tasks, daemon=True) _scheduler_thread.start() -# 程序退出清理函数 -def cleanup(): - """程序退出时执行的清理操作""" - logger.info("开始执行程序退出清理...") - - # 停止调度线程 - global _scheduler_thread_running - _scheduler_thread_running = False - - # 等待调度线程结束(最多等待5秒) - if _scheduler_thread and _scheduler_thread.is_alive(): - _scheduler_thread.join(timeout=5) - - # 保存策略数据 - try: - StrategyPositionManager.save_strategy_data() - logger.info("策略数据已保存") - except Exception as e: - logger.error(f"保存策略数据失败: {str(e)}") - - # 登出交易实例 - try: - # 登出模拟交易实例 - if _sim_trader_instance is not None: - _sim_trader_instance.logout() - logger.info("模拟交易实例已登出") - - # 登出实盘交易实例 - if _real_trader_instance is not None: - _real_trader_instance.logout() - logger.info("实盘交易实例已登出") - except Exception as e: - logger.error(f"登出交易实例失败: {str(e)}") - - logger.info("程序退出清理完成") - -# 注册程序退出处理函数 -atexit.register(cleanup) - # 初始化交易环境 get_trader().login() # 添加请求频率限制 app = Flask(__name__) -# 添加策略数据相关的定期任务 -schedule.every().day.at(Config.CLEAN_ORDERS_TIME).do(StrategyPositionManager.clean_timeout_orders) # 每天清理超时委托 -schedule.every().day.at(Config.STRATEGY_SAVE_TIME).do(StrategyPositionManager.save_strategy_data) # 每天收盘后保存策略数据 - -# 程序启动时加载策略数据 -StrategyPositionManager.load_strategy_data() - -# 程序退出时保存策略数据 -atexit.register(StrategyPositionManager.save_strategy_data) - # 使用配置文件中的时间 run_daily(Config.MARKET_OPEN_TIME, lambda: get_trader().login()) run_daily(Config.MARKET_CLOSE_TIME, lambda: get_trader().logout()) @@ -256,61 +100,56 @@ def health_check(): def buy(): """Buy an item with given parameters.""" logger.info("Received buy request") - + # 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", "") # 新增策略名称参数,默认为空 + strategy_name = data.get( + "strategy_name", "default_strategy" + ) # 新增策略名称参数,默认为空 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_sim_trader() - result = sim_trader.buy(code, price, amount) - - # 如果指定了策略名称,记录到策略持仓 - if strategy_name: - # 模拟交易立即生效,更新策略持仓 - StrategyPositionManager.update_strategy_position(sim_trader, strategy_name, code, 'buy', amount) - - return jsonify({"success": True, "data": result, "simulation": True}), 200 - + # 检查是否在交易时间内 - trading_hours, hours_message = is_trading_hours() - if not trading_hours: - logger.warning(f"实盘交易失败 - {hours_message} - 代码: {code}, 价格: {price}, 数量: {amount}") - return jsonify({"success": False, "error": f"交易失败: {hours_message},非交易时间不能实盘交易"}), 400 - - # 使用RealTraderManager执行实盘交易 - logger.info(f"使用RealTraderManager执行买入: 代码={code}, 价格={price}, 数量={amount}, 策略={strategy_name}") - rtm = get_real_trader_manager() - result = rtm.place_order(strategy_name, code, 'buy', amount, price) - - if result.get('success'): - logger.info(f"RealTraderManager买入成功: {result}") - return jsonify({"success": True, "data": result, "simulation": False}), 200 + if not get_trader().is_trading_hours(): + logger.warning( + f"交易失败 - 非交易时间不能交易 - 代码: {code}, 价格: {price}, 数量: {amount}" + ) + return ( + jsonify( + {"success": False, "error": f"交易失败: 非交易时间不能实盘交易"} + ), + 400, + ) + + if is_real_mode(): + logger.info( + f"使用RealTraderManager执行买入: 代码={code}, 价格={price}, 数量={amount}, 策略={strategy_name}" + ) + rtm = get_real_trader_manager() + result = rtm.place_order( + strategy_name, code, ORDER_DIRECTION_BUY, amount, price + ) else: - logger.error(f"RealTraderManager买入失败: {result.get('error')}") - return jsonify({"success": False, "error": result.get('error')}), 400 - + result = get_trader().buy(code, price, amount) + + if result.get("success"): + logger.info(f"买入成功: {result}") + return jsonify({"success": True, "order_id": result.get("order_id")}), 200 + else: + logger.error(f"买入失败: {result}") + return jsonify({"success": False, "error": result}), 400 + except ValueError as e: logger.error(f"Invalid request parameters: {str(e)}") abort(400, description=str(e)) @@ -323,7 +162,7 @@ def buy(): def sell(): """Sell an item with given parameters.""" logger.info("Received sell request") - + # Get data from request body data = request.get_json() code = data.get("code") @@ -334,50 +173,40 @@ def sell(): 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_sim_trader() - result = sim_trader.sell(code, price, amount) - - # 如果指定了策略名称,记录到策略持仓 - if strategy_name: - # 模拟交易下,使用简单更新模式 - StrategyPositionManager.update_strategy_position(sim_trader, strategy_name, code, 'sell', amount) - - return jsonify({"success": True, "data": result, "simulation": True}), 200 - + # 检查是否在交易时间内 - trading_hours, hours_message = is_trading_hours() - if not trading_hours: - logger.warning(f"实盘交易失败 - {hours_message} - 代码: {code}, 价格: {price}, 数量: {amount}") - return jsonify({"success": False, "error": f"交易失败: {hours_message},非交易时间不能实盘交易"}), 400 - - # 使用RealTraderManager执行实盘交易 - logger.info(f"使用RealTraderManager执行卖出: 代码={code}, 价格={price}, 数量={amount}, 策略={strategy_name}") - rtm = get_real_trader_manager() - result = rtm.place_order(strategy_name, code, 'sell', amount, price) - - if result.get('success'): - logger.info(f"RealTraderManager卖出成功: {result}") - return jsonify({"success": True, "data": result, "simulation": False}), 200 + if not get_trader().is_trading_hours(): + logger.warning( + f"交易失败 - 非交易时间不能交易 - 代码: {code}, 价格: {price}, 数量: {amount}" + ) + return ( + jsonify( + {"success": False, "error": f"交易失败: 非交易时间不能实盘交易"} + ), + 400, + ) + + if is_real_mode(): + rtm = get_real_trader_manager() + result = rtm.place_order( + strategy_name, code, ORDER_DIRECTION_SELL, amount, price + ) else: - logger.error(f"RealTraderManager卖出失败: {result.get('error')}") - return jsonify({"success": False, "error": result.get('error')}), 400 - + result = get_trader().sell(code, price, amount) + + if result.get("success"): + logger.info(f"卖出成功: {result}") + return jsonify({"success": True, "order_id": result.get("order_id")}), 200 + else: + logger.error(f"卖出失败: {result}") + return jsonify({"success": False, "error": result}), 400 + except ValueError as e: logger.error(f"Invalid request parameters: {str(e)}") abort(400, description=str(e)) @@ -386,37 +215,13 @@ def sell(): 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}") +@app.route("/yu/cancel/", methods=["DELETE"]) +def cancel(order_id): + logger.info(f"Received cancel request for entrust_no={order_id}") try: - # 不考虑是否为模拟交易,直接使用实盘 - # 使用RealTraderManager - rtm = get_real_trader_manager() - # 在RealTraderManager的待处理订单中查找 - found_in_rtm = False - for order in rtm.get_pending_orders(): - if str(order['order_id']) == str(entrust_no): - found_in_rtm = True - # 使用RealTraderManager中的trader进行撤单 - result = rtm.trader.cancel(entrust_no) - logger.info(f"通过RealTraderManager撤单结果: {result}") - - # 更新订单状态 - rtm.check_pending_orders() - return jsonify({"success": True, "data": result, "simulation": False}), 200 - - # 如果RealTraderManager中未找到,使用普通实盘撤单 - if not found_in_rtm: - logger.info(f"在RealTraderManager中未找到订单{entrust_no},使用普通实盘撤单") - real_trader = get_real_trader() - result = real_trader.cancel(entrust_no) - logger.info(f"普通实盘撤单结果: {result}") - - # 更新未完成委托状态 - StrategyPositionManager.update_pending_orders(real_trader) - return jsonify({"success": True, "data": result, "simulation": False}), 200 - + result = get_trader().cancel(order_id) + return jsonify({"success": True, "data": result}), 200 + except Exception as e: logger.error(f"Error processing cancel request: {str(e)}") abort(500, description="Internal server error") @@ -428,15 +233,10 @@ def get_balance(): logger.info("Received balance request") try: # 直接使用实盘交易实例,不考虑模拟盘 - trader = get_real_trader() - balance = execute_with_timeout(trader.get_balance, Config.TRADE_TIMEOUT) - - if balance is None: - logger.error("获取实盘余额超时") - return jsonify({"success": False, "error": "获取余额超时,请稍后重试", "simulation": False}), 500 - + balance = get_trader().get_balance() + logger.info(f"实盘交易余额: {balance}") - return jsonify({"success": True, "data": balance, "simulation": False}), 200 + return jsonify({"success": True, "data": balance}), 200 except Exception as e: logger.error(f"Error processing balance request: {str(e)}") abort(500, description="Internal server error") @@ -446,36 +246,11 @@ def get_balance(): def get_positions(): """Get the positions of the account.""" logger.info("Received positions request") - + try: - # 获取查询参数 - strategy_name = request.args.get("strategy_name", "") - - # 判断当前交易模式 - should_simulate, _ = should_use_simulation() - - # 选择相应的交易实例 - trader = get_sim_trader() if should_simulate else get_real_trader() - - # 更新未完成委托状态 - StrategyPositionManager.update_pending_orders(trader) - - # 如果实盘且指定要查询RealTraderManager中的目标持仓 - if not should_simulate and request.args.get("target", "").lower() == "true": - rtm = get_real_trader_manager() - targets = rtm.get_strategy_targets() - - # 如果指定了策略名称 - if strategy_name: - strategy_target = targets.get(strategy_name, {}) - return jsonify({"success": True, "data": {strategy_name: strategy_target}, "simulation": False}), 200 - - return jsonify({"success": True, "data": targets, "simulation": False}), 200 - - # 使用StrategyPositionManager获取持仓信息 - result = StrategyPositionManager.get_strategy_positions(trader, strategy_name if strategy_name else None) - - return jsonify({"success": True, "data": result, "simulation": should_simulate}), 200 + result = get_trader().get_positions() + + return jsonify({"success": True, "data": result}), 200 except Exception as e: logger.error(f"Error processing positions request: {str(e)}") abort(500, description="Internal server error") @@ -486,12 +261,10 @@ def get_today_trades(): """Get the today's trades of the account.""" logger.info("Received today trades request") try: - # 直接使用实盘交易实例,不考虑模拟盘 - trader = get_real_trader() - trades = trader.get_today_trades() + trades = get_trader().get_today_trades() logger.info(f"今日成交: {trades}") - return jsonify({"success": True, "data": trades, "simulation": False}), 200 + return jsonify({"success": True, "data": trades}), 200 except Exception as e: logger.error(f"Error processing today trades request: {str(e)}") abort(500, description="Internal server error") @@ -502,12 +275,10 @@ def get_today_orders(): """Get the today's entrust of the account.""" logger.info("Received today entrust request") try: - # 直接使用实盘交易实例,不考虑模拟盘 - trader = get_real_trader() - entrust = trader.get_today_orders() + entrust = get_trader().get_today_orders() logger.info(f"今日委托: {entrust}") - return jsonify({"success": True, "data": entrust, "simulation": False}), 200 + return jsonify({"success": True, "data": entrust}), 200 except Exception as e: logger.error(f"Error processing today entrust request: {str(e)}") abort(500, description="Internal server error") @@ -518,164 +289,15 @@ def clear_strategy(strategy_name): """清除指定策略的持仓管理数据""" logger.info(f"接收到清除策略持仓请求: {strategy_name}") try: - # 判断当前交易模式 - should_simulate, _ = should_use_simulation() - - # 如果是实盘模式,使用RealTraderManager - if not should_simulate: - # 清除RealTraderManager中的策略目标 - rtm = get_real_trader_manager() - if strategy_name in rtm.strategy_targets: - with _instance_lock: # 使用锁保护操作 - if strategy_name in rtm.strategy_targets: - del rtm.strategy_targets[strategy_name] - logger.info(f"已清除RealTraderManager中的策略目标: {strategy_name}") - - # 清除RealTraderManager中相关的待处理订单 - pending_orders_to_remove = [] - for order_id, order_info in rtm.pending_orders.items(): - if order_info.get('strategy_name') == strategy_name: - pending_orders_to_remove.append(order_id) - - # 删除相关订单 - for order_id in pending_orders_to_remove: - with _instance_lock: # 使用锁保护操作 - if order_id in rtm.pending_orders: - del rtm.pending_orders[order_id] - logger.info(f"已清除RealTraderManager中的订单: {order_id}") - - # 获取相应的交易实例 - trader = get_sim_trader() if should_simulate else get_real_trader() - - # 如果是模拟交易实例,则重置模拟交易实例 - if should_simulate and isinstance(trader, SimulationTrader): - with _instance_lock: # 使用锁保护操作 - global _sim_trader_instance - if _sim_trader_instance is not None: - logger.info("重置模拟交易实例") - # 创建一个新的模拟交易实例,替换原有实例 - _sim_trader_instance = SimulationTrader() - trader = _sim_trader_instance - - # 使用StrategyPositionManager清除策略 - success, message = StrategyPositionManager.clear_strategy(trader, strategy_name) - - if success: - return jsonify({"success": True, "message": message, "simulation": should_simulate}), 200 - else: - abort(400, description=message) - + get_trader().clear_position_manager(strategy_name) + + return jsonify({"success": True, "message": "clear success"}), 200 + except Exception as e: logger.error(f"清除策略持仓时出错: {str(e)}") abort(500, description="服务器内部错误") -# 超时处理函数 -def execute_with_timeout(func, timeout, *args, **kwargs): - """执行函数并设置超时时间,如果超时则返回None - - Args: - func: 要执行的函数 - timeout: 超时时间(秒) - args, kwargs: 传递给func的参数 - - Returns: - func的返回值,如果超时则返回None - """ - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(func, *args, **kwargs) - try: - return future.result(timeout=timeout) - except TimeoutError: - logger.warning(f"函数 {func.__name__} 执行超时 (>{timeout}秒)") - return None - except Exception as e: - logger.error(f"函数 {func.__name__} 执行出错: {str(e)}") - return None - - -# 添加新的API端点查询订单状态 -@app.route("/yu/order_status", methods=["GET"]) -def get_order_status(): - """获取订单状态""" - logger.info("Received order status request") - - try: - # 判断当前交易模式 - should_simulate, _ = should_use_simulation() - - if not should_simulate: - # 实盘模式,使用RealTraderManager - try: - rtm = get_real_trader_manager() - pending_orders = rtm.get_pending_orders() - if pending_orders is None: - logger.error("从RealTraderManager获取订单状态失败") - return jsonify({"success": False, "error": "获取订单状态失败", "simulation": False}), 500 - return jsonify({"success": True, "data": pending_orders, "simulation": False}), 200 - except Exception as e: - logger.error(f"从RealTraderManager获取订单状态时出错: {str(e)}") - # 发生错误时,回退到使用普通交易实例 - logger.info("回退到使用普通交易实例获取订单状态") - - trader = get_real_trader() - try: - entrusts = execute_with_timeout(trader.get_today_orders, Config.TRADE_TIMEOUT) - if entrusts is None: - logger.error("获取今日委托超时") - return jsonify({"success": False, "error": "获取今日委托超时", "simulation": False}), 500 - return jsonify({"success": True, "data": entrusts, "simulation": False}), 200 - except Exception as e: - logger.error(f"获取今日委托时出错: {str(e)}") - return jsonify({"success": False, "error": f"获取今日委托时出错: {str(e)}", "simulation": False}), 500 - else: - # 模拟交易模式 - trader = get_sim_trader() - try: - entrusts = trader.get_today_orders() - return jsonify({"success": True, "data": entrusts, "simulation": True}), 200 - except Exception as e: - logger.error(f"获取今日委托时出错: {str(e)}") - return jsonify({"success": False, "error": f"获取今日委托时出错: {str(e)}", "simulation": True}), 500 - except Exception as e: - logger.error(f"处理订单状态请求时出错: {str(e)}") - abort(500, description="Internal server error") - - -# 添加新的API端点查询策略目标持仓 -@app.route("/yu/strategy_targets", methods=["GET"]) -def get_strategy_targets(): - """获取策略目标持仓""" - logger.info("Received strategy targets request") - - try: - # 获取查询参数 - strategy_name = request.args.get("strategy_name") - - # 检查是否是实盘模式 - should_simulate, _ = should_use_simulation() - - if should_simulate: - return jsonify({"success": False, "error": "模拟交易模式下不支持目标持仓", "simulation": True}), 400 - - try: - rtm = get_real_trader_manager() - targets = rtm.get_strategy_targets() - - # 如果指定了策略名称,则只返回该策略的目标持仓 - if strategy_name: - strategy_target = targets.get(strategy_name, {}) - return jsonify({"success": True, "data": {strategy_name: strategy_target}, "simulation": False}), 200 - - return jsonify({"success": True, "data": targets, "simulation": False}), 200 - except Exception as e: - logger.error(f"获取策略目标持仓时出错: {str(e)}") - return jsonify({"success": False, "error": f"获取策略目标持仓时出错: {str(e)}", "simulation": False}), 500 - except Exception as e: - logger.error(f"处理策略目标持仓请求时出错: {str(e)}") - abort(500, description="Internal server error") - - if __name__ == "__main__": logger.info(f"Server starting on {Config.HOST}:{Config.PORT}") app.run(debug=Config.DEBUG, host=Config.HOST, port=Config.PORT) diff --git a/uv.lock b/uv.lock index 0f921ba..e40689d 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,30 @@ version = 1 revision = 1 requires-python = ">=3.12.8" +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -231,6 +255,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + [[package]] name = "ordered-set" version = "4.1.0" @@ -249,6 +282,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + [[package]] name = "pygments" version = "2.19.1" @@ -263,6 +314,7 @@ name = "real-trader" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "black" }, { name = "chinese-calendar" }, { name = "flask" }, { name = "flask-limiter" }, @@ -272,6 +324,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "black", specifier = ">=25.1.0" }, { name = "chinese-calendar", specifier = ">=1.10.0" }, { name = "flask", specifier = ">=3.1.0" }, { name = "flask-limiter", specifier = ">=3.12" },