commit;
This commit is contained in:
		
							parent
							
								
									f2400305e9
								
							
						
					
					
						commit
						326835967d
					
				|  | @ -19,3 +19,4 @@ pandas==2.2.3 | |||
| apscheduler==3.11.0 | ||||
| pymongo==4.13.0 | ||||
| scikit-learn==1.6.1 | ||||
| dbutils==3.1.2 | ||||
|  | @ -0,0 +1,52 @@ | |||
| # QMT交易配置 | ||||
| qmt_config: | ||||
|   # 客户端路径 | ||||
|   client_path: "D:\\SoftwareCenter\\中金财富QMT个人版模拟交易端38421\\userdata_mini" | ||||
|    | ||||
|   # 账户配置 | ||||
|   account: | ||||
|     account_id: "10839603"  # 资金账号 | ||||
|     account_type: "STOCK"   # 账号类型:STOCK(股票) CREDIT(信用) FUTURE(期货) | ||||
| 
 | ||||
| # 数据库配置 | ||||
| database: | ||||
|   host: "192.168.18.199" | ||||
|   port: 3306 | ||||
|   user: "root" | ||||
|   password: "Chlry#$.8" | ||||
|   database: "db_gp_cj" | ||||
| 
 | ||||
| # 日志配置 | ||||
| logging: | ||||
|   level: "INFO" | ||||
|   log_dir: "../logs" | ||||
|   log_format: "%(asctime)s - %(levelname)s - %(message)s" | ||||
| 
 | ||||
| # Redis 配置 | ||||
| redis: | ||||
|   host: "192.168.18.208" | ||||
|   port: 6379 | ||||
|   db: 13 | ||||
|   password: "wlkj2018" | ||||
|   socket_timeout: 5 | ||||
| 
 | ||||
| # 计划同步配置(从MySQL同步到Redis) | ||||
| plan_sync: | ||||
|   interval_seconds: 60  # 定时同步周期,秒 | ||||
| 
 | ||||
| # 交易策略配置 | ||||
| strategy: | ||||
|   # 买入配置 | ||||
|   buy: | ||||
|     amount: 50000  # 每只股票买入金额(元) | ||||
|     enabled: true  # 是否启用买入策略 | ||||
|      | ||||
|   # 卖出配置 | ||||
|   sell: | ||||
|     enabled: true  # 是否启用卖出策略 | ||||
|     # 卖出策略:当价格高于目标价时卖出全部持仓 | ||||
|      | ||||
|   # 订单配置 | ||||
|   order: | ||||
|     timeout: 300  # 订单超时时间(秒),默认5分钟 | ||||
|     cooldown: 60  # 同股票下单冷却时间(秒),避免频繁下单  | ||||
|  | @ -0,0 +1,52 @@ | |||
| # coding:utf-8 | ||||
| import os | ||||
| import yaml | ||||
| import sys | ||||
| 
 | ||||
| def load_config(): | ||||
|     """加载配置文件""" | ||||
|     config_path = os.path.join(os.path.dirname(__file__), 'config.yaml') | ||||
|     try: | ||||
|         with open(config_path, 'r', encoding='utf-8') as f: | ||||
|             config = yaml.safe_load(f) | ||||
|         return config | ||||
|     except Exception as e: | ||||
|         print(f"加载配置文件失败: {str(e)}") | ||||
|         sys.exit(1) | ||||
| 
 | ||||
| def get_qmt_config(): | ||||
|     """获取QMT配置""" | ||||
|     config = load_config() | ||||
|     return config['qmt_config'] | ||||
| 
 | ||||
| def get_database_config(): | ||||
|     """获取数据库配置""" | ||||
|     config = load_config() | ||||
|     return config['database'] | ||||
| 
 | ||||
| def get_logging_config(): | ||||
|     """获取日志配置""" | ||||
|     config = load_config() | ||||
|     return config['logging'] | ||||
| 
 | ||||
| def get_strategy_config(): | ||||
|     """获取策略配置""" | ||||
|     config = load_config() | ||||
|     return config.get('strategy', {}) | ||||
| 
 | ||||
| def get_database_config(): | ||||
|     """获取数据库配置""" | ||||
|     config = load_config() | ||||
|     print(config) | ||||
|     print("----------------------------------------------------------") | ||||
|     return config.get('database', {}) | ||||
| 
 | ||||
| def get_redis_config(): | ||||
|     """获取Redis配置""" | ||||
|     config = load_config() | ||||
|     return config.get('redis', {}) | ||||
| 
 | ||||
| def get_plan_sync_config(): | ||||
|     """获取计划同步配置""" | ||||
|     config = load_config() | ||||
|     return config.get('plan_sync', {'interval_seconds': 60}) | ||||
|  | @ -0,0 +1,575 @@ | |||
| # coding:utf-8 | ||||
| import pymysql | ||||
| import logging | ||||
| import datetime | ||||
| import time | ||||
| from typing import Dict, List, Optional, Tuple | ||||
| from dbutils.pooled_db import PooledDB  # 1. 引入 PooledDB | ||||
| from config_loader import get_database_config | ||||
| 
 | ||||
| # --- 连接池的配置 --- | ||||
| # 2. 创建全局唯一的数据库连接池 | ||||
| # 我们可以把连接池看作一个"连接的仓库",所有线程都从这里借用和归还连接 | ||||
| try: | ||||
|     db_config = get_database_config() | ||||
|     pool = PooledDB( | ||||
|         creator=pymysql,  # 指定使用 pymysql 作为数据库连接库 | ||||
|         mincached=2,      # 初始化时,池中至少创建的空闲连接数 | ||||
|         maxcached=5,      # 池中最多能存放的空闲连接数 | ||||
|         maxconnections=10, # 连接池允许的最大连接数 | ||||
|         blocking=True,    # 连接池中无可用连接时,是否阻塞等待 | ||||
|         host=db_config['host'], | ||||
|         port=db_config['port'], | ||||
|         user=db_config['user'], | ||||
|         password=db_config['password'], | ||||
|         database=db_config['database'], | ||||
|         charset='utf8mb4', | ||||
|         autocommit=True,  # 注意:这里设置 autocommit=True,每次执行完 SQL 会自动提交 | ||||
|         # 连接保活配置 | ||||
|         connect_timeout=10, | ||||
|         read_timeout=60, | ||||
|         write_timeout=60 | ||||
|     ) | ||||
|     # 获取一个初始化日志记录器 | ||||
|     initial_logger = logging.getLogger(__name__) | ||||
|     initial_logger.info("数据库连接池创建成功") | ||||
| except Exception as e: | ||||
|     initial_logger = logging.getLogger(__name__) | ||||
|     initial_logger.error(f"数据库连接池创建失败: {str(e)}") | ||||
|     pool = None # 创建失败,则 pool 为 None | ||||
| 
 | ||||
| def close_pool(): | ||||
|     """关闭连接池(在应用退出时调用)""" | ||||
|     if pool: | ||||
|         pool.close() | ||||
|         logging.getLogger(__name__).info("数据库连接池已关闭") | ||||
| 
 | ||||
| class DatabaseManager: | ||||
|     """ | ||||
|     数据库管理器类(已改造为使用连接池) | ||||
|     现在这个类的每个实例都是轻量级的,并且线程安全。 | ||||
|     """ | ||||
|      | ||||
|     def __init__(self, logger=None): | ||||
|         self.logger = logger or logging.getLogger(__name__) | ||||
|         if pool is None: | ||||
|             raise Exception("数据库连接池未初始化,请检查配置") | ||||
|         # 3. 每个实例共享同一个连接池 | ||||
|         self.pool = pool | ||||
|      | ||||
|     def _get_connection(self): | ||||
|         """从连接池获取一个连接""" | ||||
|         return self.pool.connection() | ||||
| 
 | ||||
|     def execute_query(self, sql: str, params: tuple = None) -> List[Tuple]: | ||||
|         """执行查询SQL(从连接池获取连接)""" | ||||
|         # 4. 每次执行都从池中获取一个新连接,用完后自动归还 | ||||
|         conn = self._get_connection() | ||||
|         try: | ||||
|             with conn.cursor() as cursor: | ||||
|                 cursor.execute(sql, params) | ||||
|                 result = cursor.fetchall() | ||||
|                 return result | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"执行查询失败: {sql}, 错误: {str(e)}") | ||||
|             # 连接池会自动处理坏掉的连接,所以通常不需要复杂的重连逻辑 | ||||
|             raise | ||||
|         finally: | ||||
|             if conn: | ||||
|                 # 5. conn.close() 在这里不是关闭连接,而是将连接【归还】给连接池 | ||||
|                 conn.close() | ||||
|      | ||||
|     def execute_update(self, sql: str, params: tuple = None) -> int: | ||||
|         """执行更新SQL(从连接池获取连接)""" | ||||
|         conn = self._get_connection() | ||||
|         try: | ||||
|             with conn.cursor() as cursor: | ||||
|                 affected_rows = cursor.execute(sql, params) | ||||
|                 # 因为设置了 autocommit=True, 所以不需要手动 conn.commit() | ||||
|                 return affected_rows | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"执行更新失败: {sql}, 错误: {str(e)}") | ||||
|             raise | ||||
|         finally: | ||||
|             if conn: | ||||
|                 # 同样,将连接归还给池 | ||||
|                 conn.close() | ||||
|      | ||||
|     # ====================================================================== | ||||
|     #  注意:以下所有业务逻辑方法,都不需要做任何修改! | ||||
|     #  因为它们都依赖于 execute_query 和 execute_update,而这两个方法已经被改造好了。 | ||||
|     # ====================================================================== | ||||
| 
 | ||||
|     def get_active_buy_plans(self, strategy_id: int = 1) -> Dict[str, Dict]: | ||||
|         """获取激活的买入计划(仅当日最新记录)""" | ||||
|         sql = """ | ||||
|         SELECT t.stock_code, t.stock_name, t.target_price, t.buy_amount, t.trading_version | ||||
|         FROM trading_buy_plan t | ||||
|         JOIN ( | ||||
|           SELECT stock_code, MAX(update_time) AS max_ut | ||||
|           FROM trading_buy_plan | ||||
|           WHERE strategy_id=%s AND is_active=1 AND DATE(trading_time)=CURDATE() | ||||
|           GROUP BY stock_code | ||||
|         ) m ON t.stock_code=m.stock_code AND t.update_time=m.max_ut | ||||
|         WHERE t.strategy_id=%s AND t.is_active=1 | ||||
|         """ | ||||
|         try: | ||||
|             results = self.execute_query(sql, (strategy_id, strategy_id)) | ||||
|             buy_plans = {} | ||||
|             for row in results: | ||||
|                 stock_code, stock_name, target_price, buy_amount, tver = row | ||||
|                 buy_plans[stock_code] = { | ||||
|                     'stock_name': stock_name, | ||||
|                     'target_price': float(target_price), | ||||
|                     'buy_amount': float(buy_amount), | ||||
|                     'trading_version': int(tver or 0) | ||||
|                 } | ||||
|             self.logger.info(f"加载买入计划: {len(buy_plans)}个") | ||||
|             return buy_plans | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"获取买入计划失败: {str(e)}") | ||||
|             return {} | ||||
|      | ||||
|     def get_active_sell_plans(self, strategy_id: int = 1) -> Dict[str, Dict]: | ||||
|         """获取激活的卖出计划(仅当日最新记录)""" | ||||
|         sql = """ | ||||
|         SELECT t.stock_code, t.stock_name, t.target_price, t.sell_quantity, t.trading_version | ||||
|         FROM trading_sell_plan t | ||||
|         JOIN ( | ||||
|           SELECT stock_code, MAX(update_time) AS max_ut | ||||
|           FROM trading_sell_plan | ||||
|           WHERE strategy_id=%s AND is_active=1 AND DATE(trading_time)=CURDATE() | ||||
|           GROUP BY stock_code | ||||
|         ) m ON t.stock_code=m.stock_code AND t.update_time=m.max_ut | ||||
|         WHERE t.strategy_id=%s AND t.is_active=1 | ||||
|         """ | ||||
|         try: | ||||
|             results = self.execute_query(sql, (strategy_id, strategy_id)) | ||||
|             sell_plans = {} | ||||
|             for row in results: | ||||
|                 stock_code, stock_name, target_price, sell_quantity, tver = row | ||||
|                 sell_plans[stock_code] = { | ||||
|                     'stock_name': stock_name, | ||||
|                     'target_price': float(target_price), | ||||
|                     'sell_quantity': sell_quantity, | ||||
|                     'trading_version': int(tver or 0) | ||||
|                 } | ||||
|             self.logger.info(f"加载卖出计划: {len(sell_plans)}个") | ||||
|             return sell_plans | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"获取卖出计划失败: {str(e)}") | ||||
|             return {} | ||||
| 
 | ||||
|     # ===== Plan versioned read (optional trading_version column) ===== | ||||
|     def get_max_plan_version(self, strategy_id: int = 1) -> int: | ||||
|         """获取买/卖计划的最大版本号(需要表中存在 trading_version 列)。若列不存在返回0。""" | ||||
|         try: | ||||
|             buy_sql = "SELECT COALESCE(MAX(trading_version), 0) FROM trading_buy_plan WHERE strategy_id=%s AND is_active=1" | ||||
|             sell_sql = "SELECT COALESCE(MAX(trading_version), 0) FROM trading_sell_plan WHERE strategy_id=%s AND is_active=1" | ||||
|             bmax = self.execute_query(buy_sql, (strategy_id,))[0][0] | ||||
|             smax = self.execute_query(sell_sql, (strategy_id,))[0][0] | ||||
|             return max(int(bmax or 0), int(smax or 0)) | ||||
|         except Exception as e: | ||||
|             self.logger.warning(f"读取最大计划版本失败,可能缺少 trading_version 列: {str(e)}") | ||||
|             return 0 | ||||
| 
 | ||||
|     def get_buy_plans_since(self, strategy_id: int, version: int) -> Dict[str, Dict]: | ||||
|         """获取版本号大于给定值的买入计划(需要 trading_version 列)。列缺失时返回全部激活计划。""" | ||||
|         try: | ||||
|             sql = """ | ||||
|             SELECT stock_code, stock_name, target_price, buy_amount, trading_version | ||||
|             FROM trading_buy_plan WHERE strategy_id=%s AND is_active=1 AND trading_version>%s | ||||
|             """ | ||||
|             results = self.execute_query(sql, (strategy_id, version)) | ||||
|             plans = {} | ||||
|             for row in results: | ||||
|                 stock_code, stock_name, target_price, buy_amount, tver = row | ||||
|                 plans[stock_code] = { | ||||
|                     'stock_name': stock_name, | ||||
|                     'target_price': float(target_price), | ||||
|                     'buy_amount': float(buy_amount), | ||||
|                     'trading_version': int(tver or 0) | ||||
|                 } | ||||
|             return plans | ||||
|         except Exception: | ||||
|             # 回退到全部激活计划 | ||||
|             return self.get_active_buy_plans(strategy_id) | ||||
| 
 | ||||
|     def get_sell_plans_since(self, strategy_id: int, version: int) -> Dict[str, Dict]: | ||||
|         """获取版本号大于给定值的卖出计划(需要 trading_version 列)。列缺失时返回全部激活计划。""" | ||||
|         try: | ||||
|             sql = """ | ||||
|             SELECT stock_code, stock_name, target_price, sell_quantity, trading_version | ||||
|             FROM trading_sell_plan WHERE strategy_id=%s AND is_active=1 AND trading_version>%s | ||||
|             """ | ||||
|             results = self.execute_query(sql, (strategy_id, version)) | ||||
|             plans = {} | ||||
|             for row in results: | ||||
|                 stock_code, stock_name, target_price, sell_quantity, tver = row | ||||
|                 plans[stock_code] = { | ||||
|                     'stock_name': stock_name, | ||||
|                     'target_price': float(target_price), | ||||
|                     'sell_quantity': sell_quantity, | ||||
|                     'trading_version': int(tver or 0) | ||||
|                 } | ||||
|             return plans | ||||
|         except Exception: | ||||
|             return self.get_active_sell_plans(strategy_id) | ||||
|      | ||||
|     def insert_order(self, order_data: Dict) -> bool: | ||||
|         """插入订单记录""" | ||||
|         sql = """ | ||||
|         INSERT INTO trading_order  | ||||
|         (order_id, strategy_id, stock_code, order_side, order_quantity, order_price, target_price, | ||||
|          order_status, order_time, order_remark) | ||||
|         VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) | ||||
|         """ | ||||
|         try: | ||||
|             params = ( | ||||
|                 order_data['order_id'], | ||||
|                 order_data['strategy_id'], | ||||
|                 order_data['stock_code'], | ||||
|                 order_data['order_side'], | ||||
|                 order_data['order_quantity'], | ||||
|                 order_data['order_price'], | ||||
|                 order_data['target_price'], | ||||
|                 order_data['order_status'], | ||||
|                 order_data['order_time'], | ||||
|                 order_data['order_remark'] | ||||
|             ) | ||||
|             self.execute_update(sql, params) | ||||
|             self.logger.info(f"订单记录已插入: {order_data['order_id']}") | ||||
|             return True | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"插入订单记录失败: {str(e)}") | ||||
|             return False | ||||
|      | ||||
|     def update_order_status(self, order_id: str, status: str, filled_quantity: int = None, filled_time: str = None) -> bool: | ||||
|         """更新订单状态""" | ||||
|         try: | ||||
|             # 添加调试信息 | ||||
|             self.logger.info(f"准备更新订单状态: order_id={order_id} (类型: {type(order_id)}), status={status}") | ||||
|              | ||||
|             # 确保 order_id 是字符串类型 | ||||
|             order_id_str = str(order_id) | ||||
|              | ||||
|             if filled_quantity is not None and filled_time: | ||||
|                 sql = """ | ||||
|                 UPDATE trading_order  | ||||
|                 SET order_status = %s, filled_quantity = %s, filled_time = %s | ||||
|                 WHERE order_id = %s OR qmt_order_id = %s | ||||
|                 """ | ||||
|                 params = (status, filled_quantity, filled_time, order_id_str, order_id_str) | ||||
|             else: | ||||
|                 sql = """ | ||||
|                 UPDATE trading_order  | ||||
|                 SET order_status = %s | ||||
|                 WHERE order_id = %s OR qmt_order_id = %s | ||||
|                 """ | ||||
|                 params = (status, order_id_str, order_id_str) | ||||
|              | ||||
|             # 添加 SQL 调试信息 | ||||
|             self.logger.info(f"执行 SQL: {sql}") | ||||
|             self.logger.info(f"参数: {params}") | ||||
|              | ||||
|             affected_rows = self.execute_update(sql, params) | ||||
|             # if affected_rows > 0: | ||||
|             self.logger.info(f"订单状态已更新: {order_id_str} -> {status}") | ||||
|             return True | ||||
|             # else: | ||||
|             #     self.logger.warning(f"订单状态更新失败,未找到订单: {order_id_str}") | ||||
|             #     return False | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"更新订单状态失败: {str(e)}") | ||||
|             return False | ||||
|      | ||||
|     def update_qmt_order_id(self, our_order_id: str, qmt_order_id: str) -> bool: | ||||
|         """更新QMT内部订单ID""" | ||||
|         try: | ||||
|             sql = """ | ||||
|             UPDATE trading_order  | ||||
|             SET qmt_order_id = %s | ||||
|             WHERE order_id = %s | ||||
|             """ | ||||
|             params = (str(qmt_order_id), our_order_id) | ||||
|              | ||||
|             affected_rows = self.execute_update(sql, params) | ||||
|             # if affected_rows > 0: | ||||
|             self.logger.info(f"QMT订单ID已更新: {our_order_id} -> {qmt_order_id}") | ||||
|             return True | ||||
|             # else: | ||||
|             #     self.logger.warning(f"更新QMT订单ID失败,未找到订单: {our_order_id}") | ||||
|             #     return False | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"更新QMT订单ID失败: {str(e)}") | ||||
|             return False | ||||
|      | ||||
|     def update_position(self, stock_code: str, quantity_change: int, price: float, is_buy: bool) -> bool: | ||||
|         """更新持仓""" | ||||
|         try: | ||||
|             # 先查询当前持仓 | ||||
|             sql_select = "SELECT total_quantity, cost_price FROM trading_position WHERE stock_code = %s" | ||||
|             results = self.execute_query(sql_select, (stock_code,)) | ||||
|              | ||||
|             if results: | ||||
|                 # 更新现有持仓 | ||||
|                 current_quantity, current_cost_price = results[0] | ||||
|                 if is_buy: | ||||
|                     # 买入:增加持仓 | ||||
|                     new_quantity = current_quantity + quantity_change | ||||
|                     if current_quantity > 0: | ||||
|                         new_cost_price = (current_cost_price * current_quantity + price * quantity_change) / new_quantity | ||||
|                     else: | ||||
|                         new_cost_price = price | ||||
|                 else: | ||||
|                     # 卖出:减少持仓 | ||||
|                     new_quantity = max(0, current_quantity - quantity_change) | ||||
|                     new_cost_price = current_cost_price if new_quantity > 0 else 0 | ||||
|                  | ||||
|                 if new_quantity > 0: | ||||
|                     sql_update = """ | ||||
|                     UPDATE trading_position  | ||||
|                     SET total_quantity = %s, cost_price = %s, update_time = NOW() | ||||
|                     WHERE stock_code = %s | ||||
|                     """ | ||||
|                     self.execute_update(sql_update, (new_quantity, new_cost_price, stock_code)) | ||||
|                 else: | ||||
|                     # 持仓为0,删除记录 | ||||
|                     sql_delete = "DELETE FROM trading_position WHERE stock_code = %s" | ||||
|                     self.execute_update(sql_delete, (stock_code,)) | ||||
|             else: | ||||
|                 # 新增持仓(仅买入时) | ||||
|                 if is_buy and quantity_change > 0: | ||||
|                     sql_insert = """ | ||||
|                     INSERT INTO trading_position  | ||||
|                     (stock_code, total_quantity, cost_price, create_time, update_time) | ||||
|                     VALUES (%s, %s, %s, NOW(), NOW()) | ||||
|                     """ | ||||
|                     self.execute_update(sql_insert, (stock_code, quantity_change, price)) | ||||
|              | ||||
|             self.logger.info(f"持仓已更新: {stock_code} {'买入' if is_buy else '卖出'} {quantity_change}股") | ||||
|             return True | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"更新持仓失败: {str(e)}") | ||||
|             return False | ||||
|      | ||||
|     def insert_trading_log(self, log_data: Dict) -> bool: | ||||
|         """插入交易日志""" | ||||
|         sql = """ | ||||
|         INSERT INTO trading_log  | ||||
|         (order_id, stock_code, log_type, log_level, message, create_time) | ||||
|         VALUES (%s, %s, %s, %s, %s, %s) | ||||
|         """ | ||||
|         try: | ||||
|             params = ( | ||||
|                 log_data.get('order_id'), | ||||
|                 log_data.get('stock_code'), | ||||
|                 log_data['log_type'], | ||||
|                 log_data['log_level'], | ||||
|                 log_data['message'], | ||||
|                 log_data['create_time'] | ||||
|             ) | ||||
|             self.execute_update(sql, params) | ||||
|             return True | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"插入交易日志失败: {str(e)}") | ||||
|             return False | ||||
|      | ||||
|     def get_current_positions(self) -> Dict[str, int]: | ||||
|         """获取当前持仓""" | ||||
|         sql = "SELECT stock_code, total_quantity FROM trading_position WHERE total_quantity > 0" | ||||
|         try: | ||||
|             results = self.execute_query(sql) | ||||
|             positions = {row[0]: row[1] for row in results} | ||||
|             self.logger.info(f"从数据库加载持仓: {positions}") | ||||
|             return positions | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"获取当前持仓失败: {str(e)}") | ||||
|             return {} | ||||
| 
 | ||||
|     def get_open_orders(self) -> Dict[str, Dict]: | ||||
|         """获取未完成订单(pending/submitted)""" | ||||
|         sql = """ | ||||
|         SELECT order_id, stock_code, order_side, order_quantity, order_price, order_status, order_time | ||||
|         FROM trading_order | ||||
|         WHERE order_status IN ('pending','submitted') | ||||
|         """ | ||||
|         try: | ||||
|             results = self.execute_query(sql) | ||||
|             orders = {} | ||||
|             for row in results: | ||||
|                 order_id, stock_code, side, qty, price, status, order_time = row | ||||
|                 orders[stock_code] = { | ||||
|                     'order_id': order_id, | ||||
|                     'order_quantity': qty, | ||||
|                     'order_price': float(price) if price is not None else None, | ||||
|                     'status': status, | ||||
|                     'side': side, | ||||
|                     'order_time': order_time.strftime('%Y-%m-%d %H:%M:%S') if order_time else None | ||||
|                 } | ||||
|             return orders | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"获取未完成订单失败: {str(e)}") | ||||
|             return {} | ||||
| 
 | ||||
|     def get_todays_orders_by_statuses(self, statuses: List[str]) -> List[Dict]: | ||||
|         """获取今日指定状态集合的订单列表""" | ||||
|         if not statuses: | ||||
|             return [] | ||||
|         placeholders = ','.join(['%s'] * len(statuses)) | ||||
|         sql = f""" | ||||
|         SELECT order_id, stock_code, order_side, order_quantity, order_price, order_status, order_time | ||||
|         FROM trading_order | ||||
|         WHERE DATE(order_time)=CURDATE() AND order_status IN ({placeholders}) | ||||
|         ORDER BY order_time ASC | ||||
|         """ | ||||
|         try: | ||||
|             results = self.execute_query(sql, tuple(statuses)) | ||||
|             orders: List[Dict] = [] | ||||
|             for row in results: | ||||
|                 order_id, stock_code, side, qty, price, status, order_time = row | ||||
|                 orders.append({ | ||||
|                     'order_id': str(order_id), | ||||
|                     'stock_code': stock_code, | ||||
|                     'side': side, | ||||
|                     'order_quantity': int(qty) if qty is not None else 0, | ||||
|                     'order_price': float(price) if price is not None else None, | ||||
|                     'order_status': status, | ||||
|                     'order_time': order_time, | ||||
|                 }) | ||||
|             return orders | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"获取今日订单失败: {str(e)}") | ||||
|             return [] | ||||
| 
 | ||||
|     # ===== 持仓快照同步(覆盖式) ===== | ||||
|     def upsert_position_snapshot(self, stock_code: str, total_quantity: int, available_quantity: int,  | ||||
|                                 frozen_quantity: int, cost_price: float, market_price: float,  | ||||
|                                 market_value: float, profit_loss: float, profit_loss_ratio: float) -> None: | ||||
|         """按快照覆盖更新单条持仓(存在则更新,不存在则插入)""" | ||||
|         sql = """ | ||||
|         INSERT INTO trading_position  | ||||
|         (stock_code, total_quantity, available_quantity, frozen_quantity, cost_price,  | ||||
|          market_price, market_value, profit_loss, profit_loss_ratio, create_time, update_time) | ||||
|         VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) | ||||
|         ON DUPLICATE KEY UPDATE  | ||||
|         total_quantity=VALUES(total_quantity),  | ||||
|         available_quantity=VALUES(available_quantity), | ||||
|         frozen_quantity=VALUES(frozen_quantity), | ||||
|         cost_price=VALUES(cost_price),  | ||||
|         market_price=VALUES(market_price), | ||||
|         market_value=VALUES(market_value), | ||||
|         profit_loss=VALUES(profit_loss), | ||||
|         profit_loss_ratio=VALUES(profit_loss_ratio), | ||||
|         update_time=NOW() | ||||
|         """ | ||||
|         self.execute_update(sql, (stock_code, total_quantity, available_quantity, frozen_quantity,  | ||||
|                                 cost_price, market_price, market_value, profit_loss, profit_loss_ratio)) | ||||
| 
 | ||||
|     def delete_positions_except(self, stock_codes: List[str]) -> None: | ||||
|         """删除不在给定集合中的所有持仓记录(用于快照清理)""" | ||||
|         if not stock_codes: | ||||
|             # 如果为空,清空整表 | ||||
|             sql = "DELETE FROM trading_position" | ||||
|             self.execute_update(sql) | ||||
|             return | ||||
|         placeholders = ','.join(['%s'] * len(stock_codes)) | ||||
|         sql = f"DELETE FROM trading_position WHERE stock_code NOT IN ({placeholders})" | ||||
|         self.execute_update(sql, tuple(stock_codes)) | ||||
| 
 | ||||
|     def update_account_funds(self, account_id: str, account_type: str, total_asset: float,  | ||||
|                            available_cash: float, frozen_cash: float, market_value: float,  | ||||
|                            profit_loss: float, profit_loss_ratio: float) -> bool: | ||||
|         """更新账户资金信息""" | ||||
|         try: | ||||
|             sql = """ | ||||
|             INSERT INTO trading_account_funds  | ||||
|             (account_id, account_type, total_asset, available_cash, frozen_cash,  | ||||
|              market_value, profit_loss, profit_loss_ratio, update_time) | ||||
|             VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW()) | ||||
|             ON DUPLICATE KEY UPDATE | ||||
|             account_type = VALUES(account_type), | ||||
|             total_asset = VALUES(total_asset), | ||||
|             available_cash = VALUES(available_cash), | ||||
|             frozen_cash = VALUES(frozen_cash), | ||||
|             market_value = VALUES(market_value), | ||||
|             profit_loss = VALUES(profit_loss), | ||||
|             profit_loss_ratio = VALUES(profit_loss_ratio), | ||||
|             update_time = NOW() | ||||
|             """ | ||||
|             params = (account_id, account_type, total_asset, available_cash,  | ||||
|                      frozen_cash, market_value, profit_loss, profit_loss_ratio) | ||||
|              | ||||
|             affected_rows = self.execute_update(sql, params) | ||||
|             if affected_rows > 0: | ||||
|                 self.logger.info(f"账户资金信息更新成功: {account_id}") | ||||
|                 return True | ||||
|             else: | ||||
|                 self.logger.warning(f"账户资金信息更新失败: {account_id}") | ||||
|                 return False | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"更新账户资金信息失败: {str(e)}") | ||||
|             return False | ||||
| 
 | ||||
|     def get_account_funds(self, account_id: str) -> Optional[Dict]: | ||||
|         """获取账户资金信息""" | ||||
|         try: | ||||
|             sql = "SELECT * FROM trading_account_funds WHERE account_id = %s ORDER BY update_time DESC LIMIT 1" | ||||
|             result = self.execute_query(sql, (account_id,)) | ||||
|              | ||||
|             if result: | ||||
|                 row = result[0] | ||||
|                 return { | ||||
|                     'id': row[0], | ||||
|                     'account_id': row[1], | ||||
|                     'account_type': row[2], | ||||
|                     'total_asset': float(row[3]), | ||||
|                     'available_cash': float(row[4]), | ||||
|                     'frozen_cash': float(row[5]), | ||||
|                     'market_value': float(row[6]), | ||||
|                     'profit_loss': float(row[7]), | ||||
|                     'profit_loss_ratio': float(row[8]), | ||||
|                     'update_time': row[9], | ||||
|                     'create_time': row[10] | ||||
|                 } | ||||
|             return None | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"获取账户资金信息失败: {str(e)}") | ||||
|             return None | ||||
| 
 | ||||
|     def get_account_funds_history(self, account_id: str, days: int = 7) -> List[Dict]: | ||||
|         """获取账户资金历史记录""" | ||||
|         try: | ||||
|             sql = """ | ||||
|             SELECT * FROM trading_account_funds  | ||||
|             WHERE account_id = %s  | ||||
|             AND update_time >= DATE_SUB(NOW(), INTERVAL %s DAY) | ||||
|             ORDER BY update_time DESC | ||||
|             """ | ||||
|             result = self.execute_query(sql, (account_id, days)) | ||||
|              | ||||
|             funds_history = [] | ||||
|             for row in result: | ||||
|                 funds_history.append({ | ||||
|                     'id': row[0], | ||||
|                     'account_id': row[1], | ||||
|                     'account_type': row[2], | ||||
|                     'total_asset': float(row[3]), | ||||
|                     'available_cash': float(row[4]), | ||||
|                     'frozen_cash': float(row[5]), | ||||
|                     'market_value': float(row[6]), | ||||
|                     'profit_loss': float(row[7]), | ||||
|                     'profit_loss_ratio': float(row[8]), | ||||
|                     'update_time': row[9], | ||||
|                     'create_time': row[10] | ||||
|                 }) | ||||
|             return funds_history | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"获取账户资金历史记录失败: {str(e)}") | ||||
|             return [] | ||||
| 
 | ||||
|     def close(self): | ||||
|         """关闭数据库连接池(注意:这里不需要关闭,因为池是全局的)""" | ||||
|         # DBUtils.PooledDB 的池是全局的,不需要在这里关闭 | ||||
|         # 应该在应用退出时调用 close_pool() 函数 | ||||
|         pass | ||||
|  | @ -0,0 +1,310 @@ | |||
| # coding:utf-8 | ||||
| import time | ||||
| import datetime | ||||
| import logging | ||||
| import os | ||||
| import sys | ||||
| from xtquant import xtdata | ||||
| from xtquant.xttrader import XtQuantTrader | ||||
| from xtquant.xttype import StockAccount | ||||
| 
 | ||||
| # 导入自定义模块 | ||||
| from config_loader import get_qmt_config, get_logging_config, get_strategy_config | ||||
| from strategy import load_initial_positions, create_buy_strategy_callback, create_sell_strategy_callback, start_account_funds_monitor, start_reconciliation_monitor, start_position_snapshot_monitor | ||||
| from redis_state_manager import RedisStateManager | ||||
| from database_manager import DatabaseManager, close_pool | ||||
| from trader_callback import MyXtQuantTraderCallback | ||||
| 
 | ||||
| def setup_logging(): | ||||
|     """设置日志配置""" | ||||
|     log_config = get_logging_config() | ||||
|      | ||||
|     # 创建logs目录(如果不存在) | ||||
|     log_dir = os.path.join(os.path.dirname(__file__), '..', 'logs') | ||||
|     os.makedirs(log_dir, exist_ok=True) | ||||
|      | ||||
|     # 生成日志文件名(包含日期时间) | ||||
|     log_filename = f"qmt_auto_buy_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.log" | ||||
|     log_filepath = os.path.join(log_dir, log_filename) | ||||
|      | ||||
|     # 配置日志格式 | ||||
|     logging.basicConfig( | ||||
|         level=getattr(logging, log_config['level']), | ||||
|         format=log_config['log_format'], | ||||
|         handlers=[ | ||||
|             logging.FileHandler(log_filepath, encoding='utf-8'), | ||||
|             logging.StreamHandler(sys.stdout)  # 同时输出到控制台 | ||||
|         ] | ||||
|     ) | ||||
|      | ||||
|     logger = logging.getLogger(__name__) | ||||
|     logger.info(f"日志文件已创建: {log_filepath}") | ||||
|     return logger | ||||
| 
 | ||||
| def interact(): | ||||
|     """执行后进入repl模式""" | ||||
|     import code | ||||
|     code.InteractiveConsole(locals=globals()).interact() | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     # 初始化日志 | ||||
|     logger = setup_logging() | ||||
|      | ||||
|     # 获取配置 | ||||
|     qmt_config = get_qmt_config() | ||||
|     strategy_config = get_strategy_config() | ||||
|     buy_amount = strategy_config.get('buy', {}).get('amount', 50000) | ||||
|      | ||||
|     logger.info("启动基于数据库的自动买入卖出策略") | ||||
|     logger.info("="*50) | ||||
|      | ||||
|     # 初始化计划:先从MySQL拉取“已发布计划”覆盖写入到Redis(一次性),然后读取Redis供订阅 | ||||
|     rsm = RedisStateManager(logger) | ||||
|     try: | ||||
|         dbm = DatabaseManager(logger) | ||||
|         db_buy = dbm.get_active_buy_plans(strategy_id=1) | ||||
|         db_sell = dbm.get_active_sell_plans(strategy_id=1) | ||||
|         # 清空Redis计划并全量覆盖 | ||||
|         rsm.clear_buy_plans() | ||||
|         rsm.clear_sell_plans() | ||||
|         for code, plan in db_buy.items(): | ||||
|             # 兼容数据库返回结构 | ||||
|             rsm.set_buy_plan(code, { | ||||
|                 'stock_name': plan.get('stock_name'), | ||||
|                 'target_price': plan['target_price'], | ||||
|                 'buy_amount': plan['buy_amount'], | ||||
|                 'is_active': 1 | ||||
|             }) | ||||
|         for code, plan in db_sell.items(): | ||||
|             rsm.set_sell_plan(code, { | ||||
|                 'stock_name': plan.get('stock_name'), | ||||
|                 'target_price': plan['target_price'], | ||||
|                 'sell_quantity': plan.get('sell_quantity'), | ||||
|                 'is_active': 1 | ||||
|             }) | ||||
|         logger.info(f"已用MySQL已发布计划初始化Redis,买入{len(db_buy)},卖出{len(db_sell)}") | ||||
|         # 设置已同步版本 | ||||
|         try: | ||||
|             max_ver = dbm.get_max_plan_version(strategy_id=1) | ||||
|             rsm.set_synced_version(max_ver) | ||||
|             logger.info(f"初始化同步版本设置为 {max_ver}") | ||||
|         except Exception as _e: | ||||
|             logger.warning(f"初始化同步版本设置失败: {_e}") | ||||
|     except Exception as e: | ||||
|         logger.warning(f"初始化Redis计划失败(将使用已有Redis计划):{str(e)}") | ||||
| 
 | ||||
|     buy_plans = rsm.get_all_buy_plans() | ||||
|     sell_plans = rsm.get_all_sell_plans() | ||||
|     logger.info(f"买入计划数量(Redis): {len(buy_plans)}") | ||||
|     logger.info(f"卖出计划数量(Redis): {len(sell_plans)}") | ||||
|     logger.info(f"每只股票买入金额: {buy_amount}元") | ||||
|     logger.info("="*50) | ||||
|      | ||||
|     # 显示买入计划 | ||||
|     if buy_plans: | ||||
|         logger.info("买入计划:") | ||||
|         for stock_code, plan_info in buy_plans.items(): | ||||
|             logger.info(f"  {stock_code} ({plan_info['stock_name']}): 目标价格 {plan_info['target_price']:.2f}, 买入金额 {plan_info['buy_amount']:.2f}元") | ||||
|      | ||||
|     # 显示卖出计划 | ||||
|     if sell_plans: | ||||
|         logger.info("卖出计划:") | ||||
|         for stock_code, plan_info in sell_plans.items(): | ||||
|             sell_qty = plan_info['sell_quantity'] if plan_info['sell_quantity'] else '全部' | ||||
|             logger.info(f"  {stock_code} ({plan_info['stock_name']}): 目标价格 {plan_info['target_price']:.2f}, 卖出数量 {sell_qty}") | ||||
|      | ||||
|     if not buy_plans and not sell_plans: | ||||
|         logger.warning("警告:买入和卖出计划都为空!请检查数据库配置。") | ||||
|      | ||||
|     logger.info("="*50) | ||||
|      | ||||
|     # 从配置文件获取客户端路径 | ||||
|     client_path = qmt_config['client_path'] | ||||
|     logger.info(f"客户端路径: {client_path}") | ||||
|      | ||||
|     # 生成session id 整数类型 同时运行的策略不能重复 | ||||
|     session_id = int(time.time()) | ||||
|      | ||||
|     xt_trader = XtQuantTrader(client_path, session_id) | ||||
|     # 开启主动请求接口的专用线程 开启后在on_stock_xxx回调函数里调用XtQuantTrader.query_xxx函数不会卡住回调线程,但是查询和推送的数据在时序上会变得不确定 | ||||
|     # 详见: http://docs.thinktrader.net/vip/pages/ee0e9b/#开启主动请求接口的专用线程 | ||||
|     # xt_trader.set_relaxed_response_order_enabled(True) | ||||
| 
 | ||||
|     # 从配置文件获取账户信息 | ||||
|     account_id = qmt_config['account']['account_id'] | ||||
|     account_type = qmt_config['account']['account_type'] | ||||
|      | ||||
|     # 创建资金账号对象 | ||||
|     acc = StockAccount(account_id, account_type) | ||||
|     logger.info(f"账户信息: {account_id} ({account_type})") | ||||
|      | ||||
|     # 创建交易回调类对象,并声明接收回调 | ||||
|     callback = MyXtQuantTraderCallback(logger) | ||||
|     xt_trader.register_callback(callback) | ||||
|     # 启动交易线程 | ||||
|     xt_trader.start() | ||||
|     # 建立交易连接,返回0表示连接成功 | ||||
|     connect_result = xt_trader.connect() | ||||
|     logger.info(f'建立交易连接,返回0表示连接成功 {connect_result}') | ||||
|     # 对交易回调进行订阅,订阅后可以收到交易主推,返回0表示订阅成功 | ||||
|     subscribe_result = xt_trader.subscribe(acc) | ||||
|     logger.info(f'对交易回调进行订阅,订阅后可以收到交易主推,返回0表示订阅成功 {subscribe_result}') | ||||
| 
 | ||||
|     # 查询账户资金 | ||||
|     account_info = xt_trader.query_stock_asset(acc) | ||||
|     print(account_info) | ||||
|     print("=========================================") | ||||
|     available_cash = account_info.m_dCash | ||||
|     logger.info(f"账户可用资金: {available_cash:.2f}元") | ||||
| 
 | ||||
|     # 启动时一次性加载并同步持仓:券商→Redis | ||||
|     initial_positions = load_initial_positions(xt_trader, acc, logger) | ||||
|     logger.info(f"初始持仓: {initial_positions}") | ||||
|     # 覆盖式同步DB未完成订单到Redis | ||||
|     try: | ||||
|         rsm = RedisStateManager(logger) | ||||
|         dbm = DatabaseManager(logger) | ||||
|         open_orders = dbm.get_open_orders() | ||||
|         rsm.clear_pending() | ||||
|         for stock_code, info in open_orders.items(): | ||||
|             rsm.set_pending(stock_code, info) | ||||
|         logger.info(f"已用DB未完成订单覆盖Redis在途订单,共 {len(open_orders)} 条") | ||||
|     except Exception as e: | ||||
|         logger.warning(f"Redis状态读取失败: {str(e)}") | ||||
| 
 | ||||
|     # 启动账户资金监控(每小时更新一次) | ||||
|     start_account_funds_monitor(xt_trader, acc, logger) | ||||
| 
 | ||||
|     # 停用订单超时监控(策略为价格触发挂单,挂单后不自动撤单) | ||||
|     logger.info("已停用订单超时监控与自动撤单逻辑:挂单成功后将持续等待成交") | ||||
| 
 | ||||
|     # 启动每10分钟对账线程(兜底:无回调或程序中断时),可在配置里调整频率 | ||||
|     start_reconciliation_monitor(xt_trader, acc, logger, interval_seconds=600) | ||||
| 
 | ||||
|     # 启动每10分钟持仓快照覆盖更新线程(只更新DB,不叠加) | ||||
|     start_position_snapshot_monitor(xt_trader, acc, logger, interval_seconds=600) | ||||
| 
 | ||||
|     # 下载股票数据 | ||||
|     xtdata.download_sector_data() | ||||
| 
 | ||||
|     # 创建买入策略回调函数 | ||||
|     buy_strategy_callback = create_buy_strategy_callback(xt_trader, acc, buy_amount, logger) | ||||
|      | ||||
|     # 创建卖出策略回调函数 | ||||
|     sell_strategy_callback = create_sell_strategy_callback(xt_trader, acc, logger) | ||||
| 
 | ||||
|     # 订阅买卖计划中的股票行情(基于Redis) | ||||
|     if buy_plans: | ||||
|         buy_code_list = list(buy_plans.keys()) | ||||
|         logger.info(f"开始订阅 {len(buy_code_list)} 只买入股票的行情...") | ||||
|         for code in buy_code_list: | ||||
|             xtdata.subscribe_quote(code, '1m', callback=buy_strategy_callback) | ||||
|             logger.info(f"已订阅买入 {code} ({buy_plans[code].get('stock_name','')}) 行情") | ||||
|     if sell_plans: | ||||
|         sell_code_list = list(sell_plans.keys()) | ||||
|         logger.info(f"开始订阅 {len(sell_code_list)} 只卖出股票的行情...") | ||||
|         for code in sell_code_list: | ||||
|             xtdata.subscribe_quote(code, '1m', callback=sell_strategy_callback) | ||||
|             logger.info(f"已订阅卖出 {code} ({sell_plans[code].get('stock_name','')}) 行情") | ||||
| 
 | ||||
|     # 计划同步线程:每60秒从MySQL同步到Redis,并直接更新订阅(不再分线程管理订阅) | ||||
|     import threading, time | ||||
|     def plan_sync_worker(): | ||||
|         interval = 60  # 固定60秒同步一次 | ||||
|         # 当前已订阅集合 | ||||
|         subscribed_buy = set(buy_plans.keys()) | ||||
|         subscribed_sell = set(sell_plans.keys()) | ||||
|         while True: | ||||
|             try: | ||||
|                 time.sleep(interval) | ||||
|                 dbm_local = DatabaseManager(logger) | ||||
|                 # 拉取当日最新激活计划 | ||||
|                 latest_buy = dbm_local.get_active_buy_plans(strategy_id=1) | ||||
|                 latest_sell = dbm_local.get_active_sell_plans(strategy_id=1) | ||||
|                 # 对比Redis,找出新增/删除/变更(版本或数值变化) | ||||
|                 curr_buy = rsm.get_all_buy_plans() | ||||
|                 curr_sell = rsm.get_all_sell_plans() | ||||
|                 latest_buy_codes = set(latest_buy.keys()) | ||||
|                 latest_sell_codes = set(latest_sell.keys()) | ||||
|                 # 新增/删除 | ||||
|                 add_buy = latest_buy_codes - set(curr_buy.keys()) | ||||
|                 del_buy = set(curr_buy.keys()) - latest_buy_codes | ||||
|                 add_sell = latest_sell_codes - set(curr_sell.keys()) | ||||
|                 del_sell = set(curr_sell.keys()) - latest_sell_codes | ||||
|                 # 变更(比较 trading_version 或关键字段) | ||||
|                 changed_buy = set() | ||||
|                 for code in (latest_buy_codes & set(curr_buy.keys())): | ||||
|                     lb = latest_buy[code] | ||||
|                     cb = curr_buy[code] | ||||
|                     if ( | ||||
|                         lb.get('trading_version', 0) != cb.get('trading_version', 0) | ||||
|                         or lb['target_price'] != cb.get('target_price') | ||||
|                         or lb['buy_amount'] != cb.get('buy_amount') | ||||
|                     ): | ||||
|                         changed_buy.add(code) | ||||
|                 changed_sell = set() | ||||
|                 for code in (latest_sell_codes & set(curr_sell.keys())): | ||||
|                     ls = latest_sell[code] | ||||
|                     cs = curr_sell[code] | ||||
|                     if ( | ||||
|                         ls.get('trading_version', 0) != cs.get('trading_version', 0) | ||||
|                         or ls['target_price'] != cs.get('target_price') | ||||
|                         or (ls.get('sell_quantity') != cs.get('sell_quantity')) | ||||
|                     ): | ||||
|                         changed_sell.add(code) | ||||
|                 # 更新Redis | ||||
|                 for code in add_buy | changed_buy: | ||||
|                     rsm.set_buy_plan(code, latest_buy[code]) | ||||
|                 for code in add_sell | changed_sell: | ||||
|                     rsm.set_sell_plan(code, latest_sell[code]) | ||||
|                 for code in del_buy: | ||||
|                     rsm.del_buy_plan(code) | ||||
|                 for code in del_sell: | ||||
|                     rsm.del_sell_plan(code) | ||||
|                 # 直接同步订阅(差分) | ||||
|                 new_buy_sub = latest_buy_codes | ||||
|                 new_sell_sub = latest_sell_codes | ||||
|                 # 买入订阅差分 | ||||
|                 for code in new_buy_sub - subscribed_buy: | ||||
|                     xtdata.subscribe_quote(code, '1m', callback=buy_strategy_callback) | ||||
|                     logger.info(f"[订阅新增-买入] {code}") | ||||
|                 for code in subscribed_buy - new_buy_sub: | ||||
|                     xtdata.unsubscribe_quote(code) | ||||
|                     logger.info(f"[取消订阅-买入] {code}") | ||||
|                 # 卖出订阅差分 | ||||
|                 for code in new_sell_sub - subscribed_sell: | ||||
|                     xtdata.subscribe_quote(code, '1m', callback=sell_strategy_callback) | ||||
|                     logger.info(f"[订阅新增-卖出] {code}") | ||||
|                 for code in subscribed_sell - new_sell_sub: | ||||
|                     xtdata.unsubscribe_quote(code) | ||||
|                     logger.info(f"[取消订阅-卖出] {code}") | ||||
|                 subscribed_buy = new_buy_sub | ||||
|                 subscribed_sell = new_sell_sub | ||||
|                 logger.info("计划同步与订阅更新完成") | ||||
|             except Exception as e: | ||||
|                 logger.warning(f"计划同步线程异常: {str(e)}") | ||||
| 
 | ||||
|     threading.Thread(target=plan_sync_worker, daemon=True).start() | ||||
| 
 | ||||
|     logger.info("策略启动完成,等待行情推送...") | ||||
|     if buy_plans: | ||||
|         logger.info("买入策略:当股票价格低于目标价格时,将自动买入指定金额") | ||||
|     if sell_plans: | ||||
|         logger.info("卖出策略:当股票价格高于目标价格时,将自动卖出指定数量") | ||||
|     logger.info("按 Ctrl+C 停止策略") | ||||
| 
 | ||||
|     # 阻塞主线程退出 | ||||
|     try: | ||||
|         xt_trader.run_forever() | ||||
|     except KeyboardInterrupt: | ||||
|         logger.info("策略已停止") | ||||
|     finally: | ||||
|         # 程序退出时清理连接池 | ||||
|         try: | ||||
|             close_pool() | ||||
|             logger.info("数据库连接池已清理") | ||||
|         except Exception as e: | ||||
|             logger.error(f"清理数据库连接池失败: {str(e)}") | ||||
|      | ||||
|     # 如果使用vscode pycharm等本地编辑器 可以进入交互模式 方便调试 (把上一行的run_forever注释掉 否则不会执行到这里) | ||||
|     # interact()  | ||||
|  | @ -0,0 +1,166 @@ | |||
| # coding:utf-8 | ||||
| import redis | ||||
| import json | ||||
| import datetime | ||||
| from typing import Dict, Optional, List | ||||
| from config_loader import get_redis_config | ||||
| 
 | ||||
| POSITIONS_KEY = 'qmt:positions'           # Hash: stock_code -> quantity | ||||
| PENDING_KEY = 'qmt:pending_orders'        # Hash: stock_code -> json(order_info) | ||||
| META_KEY = 'qmt:meta'                     # Hash: various metadata | ||||
| PLAN_BUY_KEY = 'qmt:plan:buy'             # Hash: stock_code -> json(plan) | ||||
| PLAN_SELL_KEY = 'qmt:plan:sell'           # Hash: stock_code -> json(plan) | ||||
| PLAN_VERSION_KEY = 'qmt:plan:version'     # String/integer: version bump on change | ||||
| PLAN_SYNCED_VERSION_KEY = 'qmt:plan:synced_version'  # 上次从DB同步的版本 | ||||
| 
 | ||||
| class RedisStateManager: | ||||
|     def __init__(self, logger=None): | ||||
|         self.logger = logger | ||||
|         cfg = get_redis_config() | ||||
|         pool = redis.ConnectionPool( | ||||
|             host=cfg.get('host', 'localhost'), | ||||
|             port=cfg.get('port', 6379), | ||||
|             db=cfg.get('db', 0), | ||||
|             password=cfg.get('password'), | ||||
|             socket_timeout=cfg.get('socket_timeout', 5), | ||||
|             decode_responses=True, | ||||
|         ) | ||||
|         self.r = redis.Redis(connection_pool=pool) | ||||
| 
 | ||||
|     # Positions | ||||
|     def get_all_positions(self) -> Dict[str, int]: | ||||
|         data = self.r.hgetall(POSITIONS_KEY) | ||||
|         return {k: int(v) for k, v in data.items()} if data else {} | ||||
| 
 | ||||
|     def get_position(self, stock_code: str) -> int: | ||||
|         v = self.r.hget(POSITIONS_KEY, stock_code) | ||||
|         return int(v) if v is not None else 0 | ||||
| 
 | ||||
|     def set_position(self, stock_code: str, quantity: int) -> None: | ||||
|         if quantity > 0: | ||||
|             self.r.hset(POSITIONS_KEY, stock_code, quantity) | ||||
|         else: | ||||
|             self.r.hdel(POSITIONS_KEY, stock_code) | ||||
|         self._touch_meta('positions_updated_at') | ||||
| 
 | ||||
|     def incr_position(self, stock_code: str, delta: int) -> int: | ||||
|         new_qty = self.r.hincrby(POSITIONS_KEY, stock_code, delta) | ||||
|         if new_qty <= 0: | ||||
|             self.r.hdel(POSITIONS_KEY, stock_code) | ||||
|             new_qty = 0 | ||||
|         self._touch_meta('positions_updated_at') | ||||
|         return new_qty | ||||
| 
 | ||||
|     # Pending Orders | ||||
|     def get_all_pending(self) -> Dict[str, Dict]: | ||||
|         data = self.r.hgetall(PENDING_KEY) | ||||
|         return {k: json.loads(v) for k, v in data.items()} if data else {} | ||||
| 
 | ||||
|     def get_pending(self, stock_code: str) -> Optional[Dict]: | ||||
|         v = self.r.hget(PENDING_KEY, stock_code) | ||||
|         return json.loads(v) if v else None | ||||
| 
 | ||||
|     def set_pending(self, stock_code: str, order_info: Dict) -> None: | ||||
|         self.r.hset(PENDING_KEY, stock_code, json.dumps(order_info, ensure_ascii=False)) | ||||
|         self._touch_meta('pending_updated_at') | ||||
| 
 | ||||
|     def del_pending(self, stock_code: str) -> None: | ||||
|         self.r.hdel(PENDING_KEY, stock_code) | ||||
|         self._touch_meta('pending_updated_at') | ||||
| 
 | ||||
|     def clear_pending(self) -> None: | ||||
|         self.r.delete(PENDING_KEY) | ||||
|         self._touch_meta('pending_cleared_at') | ||||
| 
 | ||||
|     # Meta | ||||
|     def _touch_meta(self, field: str) -> None: | ||||
|         self.r.hset(META_KEY, field, datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) | ||||
| 
 | ||||
|     def clear_all(self) -> None: | ||||
|         pipe = self.r.pipeline() | ||||
|         pipe.delete(POSITIONS_KEY) | ||||
|         pipe.delete(PENDING_KEY) | ||||
|         pipe.delete('qmt_order_mapping')  # 清理订单映射 | ||||
|         pipe.execute() | ||||
|         self._touch_meta('cleared_at') | ||||
|      | ||||
|     # 订单ID映射管理 | ||||
|     def set_qmt_order_mapping(self, qmt_order_id: str, our_order_id: str) -> None: | ||||
|         """建立QMT内部订单ID到我们订单ID的映射""" | ||||
|         self.r.hset('qmt_order_mapping', qmt_order_id, our_order_id) | ||||
|      | ||||
|     def get_our_order_id(self, qmt_order_id: str) -> Optional[str]: | ||||
|         """根据QMT内部订单ID获取我们的订单ID""" | ||||
|         return self.r.hget('qmt_order_mapping', qmt_order_id) | ||||
|      | ||||
|     def del_qmt_order_mapping(self, qmt_order_id: str) -> None: | ||||
|         """删除订单ID映射""" | ||||
|         self.r.hdel('qmt_order_mapping', qmt_order_id) | ||||
| 
 | ||||
|     # Plans (buy/sell) | ||||
|     def get_buy_plan(self, stock_code: str) -> Optional[Dict]: | ||||
|         v = self.r.hget(PLAN_BUY_KEY, stock_code) | ||||
|         return json.loads(v) if v else None | ||||
| 
 | ||||
|     def get_sell_plan(self, stock_code: str) -> Optional[Dict]: | ||||
|         v = self.r.hget(PLAN_SELL_KEY, stock_code) | ||||
|         return json.loads(v) if v else None | ||||
| 
 | ||||
|     def set_buy_plan(self, stock_code: str, plan: Dict) -> None: | ||||
|         self.r.hset(PLAN_BUY_KEY, stock_code, json.dumps(plan, ensure_ascii=False)) | ||||
|         self.bump_plan_version() | ||||
| 
 | ||||
|     def set_sell_plan(self, stock_code: str, plan: Dict) -> None: | ||||
|         self.r.hset(PLAN_SELL_KEY, stock_code, json.dumps(plan, ensure_ascii=False)) | ||||
|         self.bump_plan_version() | ||||
| 
 | ||||
|     def del_buy_plan(self, stock_code: str) -> None: | ||||
|         self.r.hdel(PLAN_BUY_KEY, stock_code) | ||||
|         self.bump_plan_version() | ||||
| 
 | ||||
|     def del_sell_plan(self, stock_code: str) -> None: | ||||
|         self.r.hdel(PLAN_SELL_KEY, stock_code) | ||||
|         self.bump_plan_version() | ||||
| 
 | ||||
|     def get_all_buy_plans(self) -> Dict[str, Dict]: | ||||
|         data = self.r.hgetall(PLAN_BUY_KEY) | ||||
|         return {k: json.loads(v) for k, v in data.items()} if data else {} | ||||
| 
 | ||||
|     def get_all_sell_plans(self) -> Dict[str, Dict]: | ||||
|         data = self.r.hgetall(PLAN_SELL_KEY) | ||||
|         return {k: json.loads(v) for k, v in data.items()} if data else {} | ||||
| 
 | ||||
|     def get_buy_codes(self) -> List[str]: | ||||
|         return list(self.r.hkeys(PLAN_BUY_KEY)) | ||||
| 
 | ||||
|     def get_sell_codes(self) -> List[str]: | ||||
|         return list(self.r.hkeys(PLAN_SELL_KEY)) | ||||
| 
 | ||||
|     def get_plan_version(self) -> int: | ||||
|         v = self.r.get(PLAN_VERSION_KEY) | ||||
|         try: | ||||
|             return int(v) if v is not None else 0 | ||||
|         except Exception: | ||||
|             return 0 | ||||
| 
 | ||||
|     def bump_plan_version(self) -> int: | ||||
|         return int(self.r.incr(PLAN_VERSION_KEY)) | ||||
| 
 | ||||
|     def clear_buy_plans(self) -> None: | ||||
|         self.r.delete(PLAN_BUY_KEY) | ||||
|         self.bump_plan_version() | ||||
| 
 | ||||
|     def clear_sell_plans(self) -> None: | ||||
|         self.r.delete(PLAN_SELL_KEY) | ||||
|         self.bump_plan_version() | ||||
| 
 | ||||
|     # synced version helpers | ||||
|     def get_synced_version(self) -> int: | ||||
|         v = self.r.get(PLAN_SYNCED_VERSION_KEY) | ||||
|         try: | ||||
|             return int(v) if v is not None else 0 | ||||
|         except Exception: | ||||
|             return 0 | ||||
| 
 | ||||
|     def set_synced_version(self, version: int) -> None: | ||||
|         self.r.set(PLAN_SYNCED_VERSION_KEY, version) | ||||
|  | @ -0,0 +1,601 @@ | |||
| # coding:utf-8 | ||||
| import time | ||||
| import datetime | ||||
| import threading | ||||
| import logging | ||||
| from xtquant import xtdata | ||||
| from xtquant.xttrader import XtQuantTrader | ||||
| from xtquant.xttype import StockAccount | ||||
| from xtquant import xtconstant | ||||
| from database_manager import DatabaseManager | ||||
| from redis_state_manager import RedisStateManager | ||||
| 
 | ||||
| def is_call_auction_time(now_time=None): | ||||
|     """ | ||||
|     判断当前时间是否处于开盘集合竞价时段 | ||||
|     """ | ||||
|     if not now_time: | ||||
|         now_time = datetime.datetime.now().time() | ||||
|      | ||||
|     # 开盘集合竞价时间段 | ||||
|     call_auction_start = datetime.time(9, 15) | ||||
|     call_auction_end = datetime.time(9, 25) | ||||
|      | ||||
|     return call_auction_start <= now_time <= call_auction_end | ||||
| 
 | ||||
| def is_continuous_trading_time(now_time=None): | ||||
|     """ | ||||
|     判断当前时间是否处于连续交易时段 | ||||
|     """ | ||||
|     if not now_time: | ||||
|         now_time = datetime.datetime.now().time() | ||||
|          | ||||
|     # 上午连续交易时段 | ||||
|     morning_start = datetime.time(9, 30) | ||||
|     morning_end = datetime.time(11, 30) | ||||
|      | ||||
|     # 下午连续交易时段 | ||||
|     afternoon_start = datetime.time(13, 0) | ||||
|     afternoon_end = datetime.time(14, 57) | ||||
|      | ||||
|     is_morning = morning_start <= now_time <= morning_end | ||||
|     is_afternoon = afternoon_start <= now_time <= afternoon_end | ||||
|      | ||||
|     return is_morning or is_afternoon | ||||
| 
 | ||||
| # 定义一个类 创建类的实例 作为状态的容器 | ||||
| class _a(): | ||||
|     pass | ||||
| 
 | ||||
| A = _a() | ||||
| A.hsa = xtdata.get_stock_list_in_sector('沪深A股') | ||||
| A.order_timeout = 300  # 订单超时时间(秒),默认5分钟 | ||||
| 
 | ||||
| # 数据库管理器实例 | ||||
| db_manager = None | ||||
| redis_manager = None | ||||
| 
 | ||||
| def initialize_database_manager(logger=None): | ||||
|     """初始化数据库管理器""" | ||||
|     global db_manager | ||||
|     if db_manager is None: | ||||
|         db_manager = DatabaseManager(logger) | ||||
|     return db_manager | ||||
| 
 | ||||
| def initialize_redis_manager(logger=None): | ||||
|     """初始化Redis状态管理器""" | ||||
|     global redis_manager | ||||
|     if redis_manager is None: | ||||
|         redis_manager = RedisStateManager(logger) | ||||
|     return redis_manager | ||||
| 
 | ||||
| def load_trading_plans_from_database(strategy_id=1, logger=None): | ||||
|     """已弃用:计划改为直接从Redis读取,保留此函数用于日志兼容""" | ||||
|     if logger: | ||||
|         logger.info("交易计划现改为从Redis读取,数据库仅做审计。") | ||||
|     return {}, {} | ||||
| 
 | ||||
| def load_initial_positions(xt_trader, acc, logger): | ||||
|     """启动时一次性加载初始持仓状态(券商→Redis)并返回Redis视图""" | ||||
|     try: | ||||
|         # 先从QMT获取持仓 | ||||
|         positions = xt_trader.query_stock_positions(acc) | ||||
|         qmt_positions = {pos.stock_code: pos.m_nVolume for pos in positions if pos.m_nVolume > 0} | ||||
|          | ||||
|         # 从数据库获取持仓 | ||||
|         db_manager = initialize_database_manager(logger) | ||||
|         db_positions = db_manager.get_current_positions() | ||||
| 
 | ||||
|         # 将券商持仓写入Redis为权威值;DB中但券商为0的暂不写入,避免脏数据覆盖 | ||||
|         r = initialize_redis_manager(logger) | ||||
|         # 清理Redis中不存在于券商的持仓(设为0即删除) | ||||
|         for code in list(r.get_all_positions().keys()): | ||||
|             if code not in qmt_positions: | ||||
|                 r.set_position(code, 0) | ||||
|         for code, qty in qmt_positions.items(): | ||||
|             r.set_position(code, qty) | ||||
| 
 | ||||
|         logger.info(f"初始持仓加载完成,已同步至Redis") | ||||
|         logger.info(f"  QMT持仓: {qmt_positions}") | ||||
|         logger.info(f"  数据库持仓: {db_positions}") | ||||
|         return r.get_all_positions() | ||||
|     except Exception as e: | ||||
|         logger.error(f"加载初始持仓失败: {str(e)}") | ||||
|         A.positions = {} | ||||
|         return {} | ||||
| 
 | ||||
| def update_position_in_memory(stock_code, quantity_change, is_buy=True, price=None, logger=None): | ||||
|     """以Redis为主更新持仓,同时更新数据库(不再维护内存镜像)""" | ||||
|     try: | ||||
|         # Redis持仓更新 | ||||
|         r = initialize_redis_manager(logger) | ||||
|         delta = quantity_change if is_buy else -quantity_change | ||||
|         new_qty = r.incr_position(stock_code, delta) | ||||
|          | ||||
|         # 更新数据库持仓 | ||||
|         if price is not None: | ||||
|             db_manager = initialize_database_manager(logger) | ||||
|             db_manager.update_position(stock_code, quantity_change, price, is_buy) | ||||
|          | ||||
|         if logger: | ||||
|             logger.info(f"持仓状态已更新: {stock_code} -> {new_qty}股(Redis)") | ||||
|     except Exception as e: | ||||
|         if logger: | ||||
|             logger.error(f"更新持仓状态失败: {str(e)}") | ||||
| 
 | ||||
| def add_pending_order(stock_code, order_id, order_quantity, order_price, target_price, order_side='buy', logger=None): | ||||
|     """添加在途订单:写Redis为主,同时记录数据库""" | ||||
|     try: | ||||
|         order_info = { | ||||
|             'order_id': order_id, | ||||
|             'order_quantity': order_quantity, | ||||
|             'order_price': order_price, | ||||
|             'target_price': target_price, | ||||
|             'order_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), | ||||
|             'status': 'pending', | ||||
|             'side': order_side | ||||
|         } | ||||
|         # Redis | ||||
|         r = initialize_redis_manager(logger) | ||||
|         r.set_pending(stock_code, order_info) | ||||
|          | ||||
|         # 记录到数据库 | ||||
|         db_manager = initialize_database_manager(logger) | ||||
|         order_data = { | ||||
|             'order_id': order_id, | ||||
|             'strategy_id': 1, | ||||
|             'stock_code': stock_code, | ||||
|             'order_side': order_side, | ||||
|             'order_quantity': order_quantity, | ||||
|             'order_price': order_price, | ||||
|             'target_price': target_price, | ||||
|             'order_status': 'pending', | ||||
|             'order_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), | ||||
|             'order_remark': f"{order_side.upper()}_{stock_code}_{int(time.time())}" | ||||
|         } | ||||
|         db_manager.insert_order(order_data) | ||||
|          | ||||
|         if logger: | ||||
|             logger.info(f"添加在途订单: {stock_code} -> {order_id}") | ||||
|     except Exception as e: | ||||
|         if logger: | ||||
|             logger.error(f"添加在途订单失败: {str(e)}") | ||||
| 
 | ||||
| def remove_pending_order(stock_code, logger=None): | ||||
|     """移除在途订单,同时更新数据库状态""" | ||||
|     try: | ||||
|         r = initialize_redis_manager(logger) | ||||
|         order_info = r.get_pending(stock_code) | ||||
|          | ||||
|         if order_info is None: | ||||
|             if logger: | ||||
|                 logger.warning(f"未找到在途订单: {stock_code}") | ||||
|             return | ||||
|              | ||||
|         r.del_pending(stock_code) | ||||
| 
 | ||||
|         # 更新数据库订单状态 | ||||
|         print("====================================") | ||||
|         db_manager = initialize_database_manager(logger) | ||||
|         db_manager.update_order_status(order_info['order_id'], 'completed') | ||||
| 
 | ||||
|         if logger: | ||||
|             logger.info(f"移除在途订单: {stock_code} -> {order_info['order_id']}") | ||||
|     except Exception as e: | ||||
|         if logger: | ||||
|             logger.error(f"移除在途订单失败: {str(e)}") | ||||
| 
 | ||||
| def is_stock_pending_order(stock_code): | ||||
|     """检查股票是否有在途订单(以Redis为准)""" | ||||
|     r = initialize_redis_manager() | ||||
|     return r.get_pending(stock_code) is not None | ||||
| 
 | ||||
| def check_order_timeout(xt_trader, acc, logger): | ||||
|     """已停用:本策略不再执行超时自动撤单,仅保留占位""" | ||||
|     logger.info("check_order_timeout 已停用:挂单后不再按超时撤单") | ||||
|     return | ||||
| 
 | ||||
| def start_order_timeout_monitor(xt_trader, acc, logger): | ||||
|     """已停用:不再启动订单超时监控线程""" | ||||
|     logger.info("订单超时监控已停用:不启动后台监控线程") | ||||
|     return | ||||
| 
 | ||||
| def create_buy_strategy_callback(xt_trader, acc, buy_amount, logger): | ||||
|     """创建买入策略回调函数""" | ||||
|     def f(data): | ||||
|         """ | ||||
|         实时行情回调函数 | ||||
|         检查当前价格是否低于目标价格,如果满足条件则买入 | ||||
|         """ | ||||
|         # logger.info(f"收到买入行情数据: {datetime.datetime.now()}") | ||||
|          | ||||
|         # 获取当前时间 | ||||
|         current_time = datetime.datetime.now().time() | ||||
|          | ||||
|         # 判断当前交易时段 | ||||
|         is_call_auction = is_call_auction_time(current_time) | ||||
|         is_continuous = is_continuous_trading_time(current_time) | ||||
|          | ||||
|         for stock_code, current_data in data.items(): | ||||
|             if stock_code not in A.hsa: | ||||
|                 continue | ||||
| 
 | ||||
|             # 从Redis读取买入计划 | ||||
|             r = initialize_redis_manager(logger) | ||||
|             plan_info = r.get_buy_plan(stock_code) | ||||
|             if not plan_info or plan_info.get('is_active', 1) != 1: | ||||
|                 continue | ||||
| 
 | ||||
|             try: | ||||
|                 # 获取当前价格和目标价格 | ||||
|                 current_price = current_data[0]['close'] | ||||
|                 target_price = plan_info['target_price'] | ||||
|                 buy_amount = plan_info['buy_amount'] | ||||
| 
 | ||||
|                 # logger.info(f"[买入监控] {stock_code} 当前价格: {current_price:.2f}, 目标价格: {target_price:.2f}") | ||||
| 
 | ||||
|                 # 判断是否满足买入条件:当前价格低于目标价格 | ||||
|                 if current_price < target_price: | ||||
|                      | ||||
|                     # 检查持仓(Redis) | ||||
|                     current_qty = r.get_position(stock_code) | ||||
|                     if current_qty > 0: | ||||
|                         # logger.info(f"{stock_code} 已有持仓 {current_qty}股,跳过买入") | ||||
|                         continue | ||||
|                      | ||||
|                     # 检查是否有在途订单(Redis) | ||||
|                     if is_stock_pending_order(stock_code): | ||||
|                         logger.info(f"{stock_code} 有在途订单,跳过买入") | ||||
|                         continue | ||||
| 
 | ||||
|                     # 集合竞价时段:只观察,不下单 | ||||
|                     if is_call_auction: | ||||
|                         logger.info(f"[集合竞价观察] {stock_code} 当前价格 {current_price:.2f} < 目标价格 {target_price:.2f},等待连续交易时段下单") | ||||
|                         continue | ||||
|                      | ||||
|                     # 连续交易时段:执行下单 | ||||
|                     if is_continuous: | ||||
|                         logger.info(f"[连续交易下单] 满足买入条件!{stock_code} 当前价格 {current_price:.2f} < 目标价格 {target_price:.2f}") | ||||
| 
 | ||||
|                         # 计算买入数量(从数据库获取金额) | ||||
|                         buy_volume = int(buy_amount / current_price / 100) * 100  # 取整为100的整数倍 | ||||
| 
 | ||||
|                         if buy_volume > 0: | ||||
|                             logger.info(f"准备买入 {stock_code} {buy_volume}股,金额约 {buy_volume * current_price:.2f}元") | ||||
| 
 | ||||
|                             # 生成订单ID | ||||
|                             order_id = f"BUY_{stock_code}_{int(time.time())}" | ||||
| 
 | ||||
|                             # 执行买入订单 | ||||
|                             async_seq = xt_trader.order_stock_async( | ||||
|                                 acc, | ||||
|                                 stock_code, | ||||
|                                 xtconstant.STOCK_BUY, | ||||
|                                 buy_volume, | ||||
|                                 xtconstant.FIX_PRICE, | ||||
|                                 target_price, | ||||
|                                 'auto_buy_strategy', | ||||
|                                 order_id | ||||
|                             ) | ||||
| 
 | ||||
|                             # 添加到在途订单(同时记录到数据库) | ||||
|                             add_pending_order(stock_code, order_id, buy_volume, current_price, target_price, 'buy', logger) | ||||
|                              | ||||
|                             logger.info(f"已提交买入订单: {stock_code} {buy_volume}股,价格{target_price},订单ID: {order_id}") | ||||
|                         else: | ||||
|                             logger.warning(f"买入数量计算为0,跳过 {stock_code}") | ||||
|                     else: | ||||
|                         # 非交易时段 | ||||
|                         logger.info(f"[非交易时段] {stock_code} 当前价格 {current_price:.2f} < 目标价格 {target_price:.2f},等待交易时段") | ||||
| 
 | ||||
|             except Exception as e: | ||||
|                 logger.error(f"处理 {stock_code} 买入行情数据时出错: {str(e)}") | ||||
|                 continue | ||||
|      | ||||
|     return f | ||||
| 
 | ||||
| def create_sell_strategy_callback(xt_trader, acc, logger): | ||||
|     """创建卖出策略回调函数""" | ||||
|     def f(data): | ||||
|         """ | ||||
|         实时行情回调函数 | ||||
|         检查当前价格是否高于目标价格,如果满足条件且持有该股票则卖出 | ||||
|         """ | ||||
|         # logger.info(f"收到卖出行情数据: {datetime.datetime.now()}") | ||||
|          | ||||
|         # 获取当前时间 | ||||
|         current_time = datetime.datetime.now().time() | ||||
|          | ||||
|         # 判断当前交易时段 | ||||
|         is_call_auction = is_call_auction_time(current_time) | ||||
|         is_continuous = is_continuous_trading_time(current_time) | ||||
|          | ||||
|         for stock_code, current_data in data.items(): | ||||
|             if stock_code not in A.hsa: | ||||
|                 continue | ||||
| 
 | ||||
|             # 从Redis读取卖出计划 | ||||
|             r = initialize_redis_manager(logger) | ||||
|             plan_info = r.get_sell_plan(stock_code) | ||||
|             if not plan_info or plan_info.get('is_active', 1) != 1: | ||||
|                 continue | ||||
| 
 | ||||
|             try: | ||||
|                 # 获取当前价格和目标价格 | ||||
|                 current_price = current_data[0]['close'] | ||||
|                 target_price = plan_info['target_price'] | ||||
| 
 | ||||
|                 # logger.info(f"[卖出监控] {stock_code} 当前价格: {current_price:.2f}, 目标价格: {target_price:.2f}") | ||||
| 
 | ||||
|                 # 判断是否满足卖出条件:当前价格高于目标价格 | ||||
|                 if current_price > target_price: | ||||
|                      | ||||
|                     # 检查持仓(Redis) | ||||
|                     r = initialize_redis_manager(logger) | ||||
|                     current_position = r.get_position(stock_code) | ||||
|                     if current_position <= 0: | ||||
|                         logger.info(f"{stock_code} 无持仓,跳过卖出") | ||||
|                         continue | ||||
|                      | ||||
|                     # 检查是否有在途订单 | ||||
|                     if is_stock_pending_order(stock_code): | ||||
|                         logger.info(f"{stock_code} 有在途订单,跳过卖出") | ||||
|                         continue | ||||
| 
 | ||||
|                     # 集合竞价时段:只观察,不下单 | ||||
|                     if is_call_auction: | ||||
|                         logger.info(f"[集合竞价观察] {stock_code} 当前价格 {current_price:.2f} > 目标价格 {target_price:.2f},等待连续交易时段下单") | ||||
|                         continue | ||||
|                      | ||||
|                     # 连续交易时段:执行下单 | ||||
|                     if is_continuous: | ||||
|                         logger.info(f"[连续交易下单] 满足卖出条件!{stock_code} 当前价格 {current_price:.2f} > 目标价格 {target_price:.2f}") | ||||
| 
 | ||||
|                         # 卖出数量:全部持仓 | ||||
|                         sell_volume = current_position | ||||
| 
 | ||||
|                         if sell_volume > 0: | ||||
|                             logger.info(f"准备卖出 {stock_code} {sell_volume}股,金额约 {sell_volume * current_price:.2f}元") | ||||
| 
 | ||||
|                             # 生成订单ID | ||||
|                             order_id = f"SELL_{stock_code}_{int(time.time())}" | ||||
|                             # 执行卖出订单 | ||||
|                             async_seq = xt_trader.order_stock_async( | ||||
|                                 acc, | ||||
|                                 stock_code, | ||||
|                                 xtconstant.STOCK_SELL, | ||||
|                                 sell_volume, | ||||
|                                 xtconstant.LATEST_PRICE, | ||||
|                                 target_price, | ||||
|                                 'auto_sell_strategy', | ||||
|                                 order_id | ||||
|                             ) | ||||
| 
 | ||||
|                             # 添加到在途订单(同时记录到数据库) | ||||
|                             add_pending_order(stock_code, order_id, sell_volume, current_price, target_price, 'sell', logger) | ||||
|                              | ||||
|                             logger.info(f"已提交卖出订单: {stock_code} {sell_volume}股,价格{target_price},订单ID: {order_id}") | ||||
|                         else: | ||||
|                             logger.warning(f"卖出数量为0,跳过 {stock_code}") | ||||
|                     else: | ||||
|                         # 非交易时段 | ||||
|                         logger.info(f"[非交易时段] {stock_code} 当前价格 {current_price:.2f} > 目标价格 {target_price:.2f},等待交易时段") | ||||
| 
 | ||||
|             except Exception as e: | ||||
|                 logger.error(f"处理 {stock_code} 卖出行情数据时出错: {str(e)}") | ||||
|                 continue | ||||
|      | ||||
|     return f  | ||||
| 
 | ||||
| def update_account_funds_periodically(xt_trader, acc, logger): | ||||
|     """每小时更新账户资金信息到数据库""" | ||||
|     while True: | ||||
|         try: | ||||
|             # 获取账户资金信息 | ||||
|             account_info = xt_trader.query_stock_asset(acc) | ||||
|              | ||||
|             if account_info: | ||||
| 
 | ||||
|                 # 计算相关数据 | ||||
|                 available_cash = getattr(account_info, 'm_dCash', 0.0) | ||||
|                 frozen_cash = getattr(account_info, 'm_dFrozenCash', 0.0) | ||||
|                 market_value = getattr(account_info, 'm_dMarketValue', 0.0) | ||||
|                 total_asset = available_cash + frozen_cash + market_value | ||||
|                  | ||||
|                 # 尝试获取盈亏信息,如果属性不存在则设为0 | ||||
|                 try: | ||||
|                     profit_loss = getattr(account_info, 'm_dProfitLoss', 0.0) | ||||
|                 except AttributeError: | ||||
|                     # 如果m_dProfitLoss不存在,尝试其他可能的属性名 | ||||
|                     profit_loss = getattr(account_info, 'm_dProfit', 0.0) | ||||
|                  | ||||
|                 # 计算盈亏比例 | ||||
|                 if total_asset > 0: | ||||
|                     profit_loss_ratio = (profit_loss / total_asset) * 100 | ||||
|                 else: | ||||
|                     profit_loss_ratio = 0.0 | ||||
|                  | ||||
|                 # 更新到数据库 | ||||
|                 db_manager = initialize_database_manager(logger) | ||||
|                 success = db_manager.update_account_funds( | ||||
|                     account_id=acc.account_id, | ||||
|                     account_type="acc.account_type", | ||||
|                     total_asset=total_asset, | ||||
|                     available_cash=available_cash, | ||||
|                     frozen_cash=frozen_cash, | ||||
|                     market_value=market_value, | ||||
|                     profit_loss=profit_loss, | ||||
|                     profit_loss_ratio=profit_loss_ratio | ||||
|                 ) | ||||
|                  | ||||
|                 if success: | ||||
|                     logger.info(f"账户资金更新成功 - 总资产: {total_asset:.2f}, 可用资金: {available_cash:.2f}, 市值: {market_value:.2f}") | ||||
|                 else: | ||||
|                     logger.warning("账户资金更新失败") | ||||
|             else: | ||||
|                 logger.warning("无法获取账户资金信息") | ||||
|                  | ||||
|         except Exception as e: | ||||
|             logger.error(f"更新账户资金信息时出错: {str(e)}") | ||||
| 
 | ||||
|         # 等待1小时(3600秒) | ||||
|         time.sleep(3600) | ||||
| 
 | ||||
| def start_account_funds_monitor(xt_trader, acc, logger): | ||||
|     """启动账户资金监控线程(每小时更新一次)""" | ||||
|     try: | ||||
|         funds_thread = threading.Thread( | ||||
|             target=update_account_funds_periodically,  | ||||
|             args=(xt_trader, acc, logger),  | ||||
|             daemon=True | ||||
|         ) | ||||
|         funds_thread.start() | ||||
|         logger.info("账户资金监控线程已启动(每小时更新一次)") | ||||
|     except Exception as e: | ||||
|         logger.error(f"启动账户资金监控失败: {str(e)}") | ||||
| 
 | ||||
| # ========== 每5分钟对账:查询券商持仓并反向更新订单/持仓 ========== | ||||
| def reconcile_orders_and_positions(xt_trader, acc, logger): | ||||
|     """一次性对账(基于当日成交对比委托): | ||||
|     - 仅查询MySQL今日订单(submitted、filled[部分]、pending、ordered) | ||||
|     - 通过QMT接口获取当日成交明细/汇总,按股票累计成交量对比委托量: | ||||
|         * 当日成交量 >= 委托量 => 状态置为 completed | ||||
|         * 0 < 当日成交量 < 委托量 => 状态置为 filled(部分成交),记录 filled_quantity | ||||
|         * 当日成交量 == 0 => 不处理 | ||||
|     - 本函数不更新持仓,持仓改由快照线程覆盖更新 | ||||
|     """ | ||||
|     try: | ||||
|         dbm = initialize_database_manager(logger) | ||||
|         # 1) 读取今日未完成订单(四种状态:submitted、filled(部分成交)、pending、委托订单) | ||||
|         # 你提到“委托订单”第四种状态名,这里按 'ordered' 兜底;如你表里具体值不同,可替换。 | ||||
|         candidate_statuses = ['submitted', 'filled', 'pending', 'ordered'] | ||||
|         orders = dbm.get_todays_orders_by_statuses(candidate_statuses) | ||||
| 
 | ||||
|         if not orders: | ||||
|             return | ||||
| 
 | ||||
|         # 2) 通过QMT查询当日成交结果,并按股票聚合成交数量 | ||||
|         filled_volume_by_code = {} | ||||
|         trades = xt_trader.query_stock_orders(acc, cancelable_only = False) | ||||
| 
 | ||||
|         for tr in trades or []: | ||||
|             code = getattr(tr, 'stock_code', None) or getattr(tr, 'm_strInstrumentID', None) | ||||
|             vol = int(getattr(tr, 'traded_volume', 0) or getattr(tr, 'm_nVolume', 0) or 0) | ||||
|             if not code or vol <= 0: | ||||
|                 continue | ||||
|             filled_volume_by_code[code] = filled_volume_by_code.get(code, 0) + vol | ||||
| 
 | ||||
|         # 3) 遍历订单并根据“当日成交累计 vs 委托量”判断状态 | ||||
|         for od in orders: | ||||
|             try: | ||||
|                 stock_code = od['stock_code'] | ||||
|                 side = od['side'] | ||||
|                 order_id = od['order_id'] | ||||
|                 qty = int(od.get('order_quantity') or 0) | ||||
|                 day_filled = int(filled_volume_by_code.get(stock_code, 0)) | ||||
| 
 | ||||
|                 if day_filled <= 0: | ||||
|                     continue | ||||
| 
 | ||||
|                 if day_filled >= qty and qty > 0: | ||||
|                     # 全部成交 -> completed | ||||
|                     dbm.update_order_status(order_id, 'completed', qty, datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) | ||||
|                     logger.info(f"[对账-简] 全部成交(>=委托): {stock_code} {side} {day_filled}/{qty}") | ||||
|                 elif 0 < day_filled < qty: | ||||
|                     # 部分成交 -> filled(部分成交) | ||||
|                     dbm.update_order_status(order_id, 'filled', day_filled, datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) | ||||
|                     logger.info(f"[对账-简] 部分成交(<委托): {stock_code} {side} {day_filled}/{qty}") | ||||
|             except Exception as ie: | ||||
|                 logger.warning(f"[对账-简] 处理订单失败 {od}: {ie}") | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         if logger: | ||||
|             logger.error(f"对账过程出错: {str(e)}") | ||||
| 
 | ||||
| def start_reconciliation_monitor(xt_trader, acc, logger, interval_seconds: int = 300): | ||||
|     """启动对账后台线程(默认每5分钟一次)""" | ||||
|     try: | ||||
|         def worker(): | ||||
|             while True: | ||||
|                 try: | ||||
|                     reconcile_orders_and_positions(xt_trader, acc, logger) | ||||
|                 except Exception as ie: | ||||
|                     logger.warning(f"对账线程异常: {ie}") | ||||
|                 time.sleep(interval_seconds) | ||||
| 
 | ||||
|         t = threading.Thread(target=worker, daemon=True) | ||||
|         t.start() | ||||
|         logger.info(f"对账线程已启动(每{interval_seconds}秒)") | ||||
|     except Exception as e: | ||||
|         logger.error(f"启动对账线程失败: {str(e)}") | ||||
| 
 | ||||
| # ========== 每5分钟持仓快照覆盖更新(仅DB) ========== | ||||
| def refresh_positions_snapshot(xt_trader, acc, logger): | ||||
|     """从QMT拉取当前持仓,覆盖更新到DB的 trading_position: | ||||
|     - 对存在的标的 upsert 所有持仓字段(数量、可用、冻结、成本价、市价、市值、盈亏等) | ||||
|     - 删除DB中多余的标的 | ||||
|     本函数不操作Redis。 | ||||
|     """ | ||||
|     try: | ||||
|         dbm = initialize_database_manager(logger) | ||||
|         positions = xt_trader.query_stock_positions(acc) or [] | ||||
|         valid_codes = [] | ||||
|         for pos in positions: | ||||
|             try: | ||||
|                 code = pos.stock_code | ||||
|                 # 获取持仓基本信息 | ||||
|                 total_qty = int(getattr(pos, 'm_nVolume', 0) or 0) | ||||
|                 available_qty = int(getattr(pos, 'm_nCanUseVolume', 0) or 0) | ||||
|                 frozen_qty = max(0, total_qty - available_qty)  # 冻结数量 = 总数量 - 可用数量 | ||||
|                  | ||||
|                 # 获取价格和市值信息 | ||||
|                 cost_price = float(getattr(pos, 'm_dOpenPrice', 0.0) or 0.0) | ||||
|                 market_price = float(getattr(pos, 'm_dLastPrice', 0.0) or 0.0) | ||||
|                 market_value = float(getattr(pos, 'm_dInstrumentValue', 0.0) or 0.0) | ||||
|                  | ||||
|                 # 获取盈亏信息 | ||||
|                 profit_loss = float(getattr(pos, 'm_dPositionProfit', 0.0) or 0.0) | ||||
|                 position_cost = float(getattr(pos, 'm_dPositionCost', 0.0) or 0.0) | ||||
|                  | ||||
|                 # 计算盈亏比例 | ||||
|                 if position_cost > 0: | ||||
|                     profit_loss_ratio = (profit_loss / position_cost) * 100 | ||||
|                 else: | ||||
|                     profit_loss_ratio = 0.0 | ||||
|                  | ||||
|                 if total_qty > 0: | ||||
|                     dbm.upsert_position_snapshot( | ||||
|                         code, total_qty, available_qty, frozen_qty,  | ||||
|                         cost_price, market_price, market_value,  | ||||
|                         profit_loss, profit_loss_ratio | ||||
|                     ) | ||||
|                     valid_codes.append(code) | ||||
|             except Exception as e: | ||||
|                 logger.warning(f"处理持仓记录失败 {getattr(pos, 'stock_code', 'unknown')}: {e}") | ||||
|                 continue | ||||
|         # 清理非持有标的 | ||||
|         dbm.delete_positions_except(valid_codes) | ||||
|         logger.info(f"[持仓快照] 已覆盖更新 {len(valid_codes)} 条持仓记录") | ||||
|     except Exception as e: | ||||
|         if logger: | ||||
|             logger.error(f"持仓快照刷新失败: {str(e)}") | ||||
| 
 | ||||
| def start_position_snapshot_monitor(xt_trader, acc, logger, interval_seconds: int = 300): | ||||
|     try: | ||||
|         def worker(): | ||||
|             while True: | ||||
|                 try: | ||||
|                     refresh_positions_snapshot(xt_trader, acc, logger) | ||||
|                 except Exception as ie: | ||||
|                     logger.warning(f"持仓快照线程异常: {ie}") | ||||
|                 time.sleep(interval_seconds) | ||||
|         t = threading.Thread(target=worker, daemon=True) | ||||
|         t.start() | ||||
|         logger.info(f"持仓快照线程已启动(每{interval_seconds}秒)") | ||||
|     except Exception as e: | ||||
|         logger.error(f"启动持仓快照线程失败: {str(e)}") | ||||
| 
 | ||||
| # 为了向后兼容,保留原来的函数名 | ||||
| def create_strategy_callback(xt_trader, acc, buy_amount, logger): | ||||
|     """创建策略回调函数(兼容性函数,实际调用买入回调)""" | ||||
|     return create_buy_strategy_callback(xt_trader, acc, buy_amount, logger) | ||||
|  | @ -0,0 +1,184 @@ | |||
| # coding:utf-8 | ||||
| import datetime | ||||
| import sys | ||||
| from xtquant.xttrader import XtQuantTraderCallback | ||||
| from strategy import update_position_in_memory, remove_pending_order | ||||
| from database_manager import DatabaseManager | ||||
| 
 | ||||
| class MyXtQuantTraderCallback(XtQuantTraderCallback): | ||||
|     def __init__(self, logger): | ||||
|         self.logger = logger | ||||
|         self.db_manager = DatabaseManager(logger) | ||||
| 
 | ||||
|     def on_disconnected(self): | ||||
|         """ | ||||
|         连接断开 | ||||
|         :return: | ||||
|         """ | ||||
|         self.logger.warning(f"{datetime.datetime.now()} 连接断开回调") | ||||
| 
 | ||||
|     def on_stock_order(self, order): | ||||
|         """ | ||||
|         委托回报推送 | ||||
|         :param order: XtOrder对象 | ||||
|         :return: | ||||
|         """ | ||||
|         self.logger.info(f"{datetime.datetime.now()} 委托回调 {order.order_remark}") | ||||
|          | ||||
|         # 更新在途订单状态和数据库 | ||||
|         try: | ||||
|             # 从订单备注中提取股票代码 | ||||
|             order_remark = order.order_remark | ||||
|             if '_' in order_remark: | ||||
|                 stock_code = order_remark.split('_')[1]  # 修正:取第二段作为股票代码 | ||||
|                  | ||||
|                 # 检查Redis中是否存在该在途订单 | ||||
|                 from strategy import initialize_redis_manager | ||||
|                 r = initialize_redis_manager(self.logger) | ||||
|                 order_info = r.get_pending(stock_code) | ||||
|                  | ||||
|                 if order_info: | ||||
|                     # 确保 order_id 是字符串类型 | ||||
|                     qmt_order_id_str = str(order.order_id) | ||||
|                     our_order_id = order_info.get('order_id') | ||||
|                      | ||||
|                     # 先更新QMT订单ID映射 | ||||
|                     if our_order_id: | ||||
|                         self.db_manager.update_qmt_order_id(our_order_id, qmt_order_id_str) | ||||
|                      | ||||
|                     # 更新数据库订单状态 | ||||
|                     self.db_manager.update_order_status(qmt_order_id_str, 'submitted') | ||||
|                      | ||||
|                     # 记录交易日志 | ||||
|                     log_data = { | ||||
|                         'order_id': our_order_id or qmt_order_id_str, | ||||
|                         'stock_code': stock_code, | ||||
|                         'log_type': 'order_submitted', | ||||
|                         'log_level': 'INFO', | ||||
|                         'message': f'订单已提交: {order.order_remark}', | ||||
|                         'create_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') | ||||
|                     } | ||||
|                     self.db_manager.insert_trading_log(log_data) | ||||
|                      | ||||
|                     self.logger.info(f"订单状态更新为已提交: {stock_code}") | ||||
|                 else: | ||||
|                     self.logger.warning(f"未找到在途订单: {stock_code}") | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"更新订单状态失败: {str(e)}") | ||||
| 
 | ||||
|     def on_stock_trade(self, trade): | ||||
|         """ | ||||
|         成交变动推送 | ||||
|         :param trade: XtTrade对象 | ||||
|         :return: | ||||
|         """ | ||||
|         self.logger.info(f"{datetime.datetime.now()} 成交回调 {trade.order_remark} 成交价格 {trade.traded_price} 成交数量 {trade.traded_volume}") | ||||
|          | ||||
|         # 更新内存中的持仓状态和数据库 | ||||
|         try: | ||||
|             # 根据订单备注判断是买入还是卖出 | ||||
|             is_buy = True  # 默认买入 | ||||
|             if trade.order_remark.startswith('SELL_'): | ||||
|                 is_buy = False | ||||
|             elif trade.order_remark.startswith('BUY_'): | ||||
|                 is_buy = True | ||||
|              | ||||
|             # 记录交易方向 | ||||
|             trade_direction = "买入" if is_buy else "卖出" | ||||
|             self.logger.info(f"识别交易方向: {trade_direction}") | ||||
|              | ||||
|             # 更新内存和数据库持仓状态 | ||||
|             update_position_in_memory(trade.stock_code, trade.traded_volume, is_buy, trade.traded_price, self.logger) | ||||
|              | ||||
|             # 确保 order_id 是字符串类型 | ||||
|             order_id_str = str(trade.order_id) | ||||
|              | ||||
|             # 更新数据库订单状态 | ||||
|             self.db_manager.update_order_status( | ||||
|                 order_id_str,  | ||||
|                 'filled',  | ||||
|                 trade.traded_volume,  | ||||
|                 datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') | ||||
|             ) | ||||
|              | ||||
|             # 记录交易日志 | ||||
|             log_data = { | ||||
|                 'order_id': order_id_str, | ||||
|                 'stock_code': trade.stock_code, | ||||
|                 'log_type': 'trade_filled', | ||||
|                 'log_level': 'INFO', | ||||
|                 'message': f'{trade_direction}成交: {trade.stock_code} {trade.traded_volume}股 @ {trade.traded_price}元', | ||||
|                 'create_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') | ||||
|             } | ||||
|             self.db_manager.insert_trading_log(log_data) | ||||
|              | ||||
|             # 从在途订单中移除 | ||||
|             remove_pending_order(trade.stock_code, self.logger) | ||||
|              | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"更新持仓状态失败: {str(e)}") | ||||
| 
 | ||||
|     def on_order_error(self, order_error): | ||||
|         """ | ||||
|         委托失败推送 | ||||
|         :param order_error:XtOrderError 对象 | ||||
|         :return: | ||||
|         """ | ||||
|         self.logger.error(f"委托报错回调 {order_error.order_remark} {order_error.error_msg}") | ||||
|          | ||||
|         # 从在途订单中移除失败的订单,并更新数据库 | ||||
|         try: | ||||
|             order_remark = order_error.order_remark | ||||
|             if '_' in order_remark: | ||||
|                 stock_code = order_remark.split('_')[1]  # 修正:取第二段作为股票代码 | ||||
|                  | ||||
|                 # 确保 order_id 是字符串类型 | ||||
|                 order_id_str = str(order_error.order_id) | ||||
|                  | ||||
|                 # 更新数据库订单状态 | ||||
|                 self.db_manager.update_order_status(order_id_str, 'failed') | ||||
|                  | ||||
|                 # 记录错误日志 | ||||
|                 log_data = { | ||||
|                     'order_id': order_id_str, | ||||
|                     'stock_code': stock_code, | ||||
|                     'log_type': 'order_failed', | ||||
|                     'log_level': 'ERROR', | ||||
|                     'message': f'订单失败: {order_error.error_msg}', | ||||
|                     'create_time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') | ||||
|                 } | ||||
|                 self.db_manager.insert_trading_log(log_data) | ||||
|                  | ||||
|                 remove_pending_order(stock_code, self.logger) | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"处理失败订单失败: {str(e)}") | ||||
| 
 | ||||
|     def on_cancel_error(self, cancel_error): | ||||
|         """ | ||||
|         撤单失败推送 | ||||
|         :param cancel_error: XtCancelError 对象 | ||||
|         :return: | ||||
|         """ | ||||
|         self.logger.error(f"{datetime.datetime.now()} {sys._getframe().f_code.co_name}") | ||||
| 
 | ||||
|     def on_order_stock_async_response(self, response): | ||||
|         """ | ||||
|         异步下单回报推送 | ||||
|         :param response: XtOrderResponse 对象 | ||||
|         :return: | ||||
|         """ | ||||
|         self.logger.info(f"异步委托回调 {response.order_remark}") | ||||
| 
 | ||||
|     def on_cancel_order_stock_async_response(self, response): | ||||
|         """ | ||||
|         :param response: XtCancelOrderResponse 对象 | ||||
|         :return: | ||||
|         """ | ||||
|         self.logger.info(f"{datetime.datetime.now()} {sys._getframe().f_code.co_name}") | ||||
| 
 | ||||
|     def on_account_status(self, status): | ||||
|         """ | ||||
|         :param response: XtAccountStatus 对象 | ||||
|         :return: | ||||
|         """ | ||||
|         self.logger.info(f"{datetime.datetime.now()} {sys._getframe().f_code.co_name}")  | ||||
|  | @ -127,7 +127,7 @@ class FinancialDataCollectorV2: | |||
|             List[str]: 股票代码列表 | ||||
|         """ | ||||
|         try: | ||||
|             query = "SELECT DISTINCT gp_code_two FROM gp_code_all_copy1 WHERE gp_code_two IS NOT NULL AND gp_code_two != ''" | ||||
|             query = "SELECT DISTINCT gp_code_two FROM gp_code_all WHERE gp_code_two IS NOT NULL AND gp_code_two != ''" | ||||
|              | ||||
|             with self.mysql_engine.connect() as conn: | ||||
|                 df = pd.read_sql(text(query), conn) | ||||
|  | @ -224,11 +224,11 @@ class FinancialDataCollectorV2: | |||
|             # 简化逻辑:直接从2025年开始往前推21个季度 | ||||
|             base_year = 2025 | ||||
|             base_quarters = [ | ||||
|                 (2025, 3), (2024, 12), (2024, 9), (2024, 6), (2024, 3), | ||||
|                 (2025, 6), (2025, 3), (2024, 12), (2024, 9), (2024, 6), (2024, 3), | ||||
|                 (2023, 12), (2023, 9), (2023, 6), (2023, 3), | ||||
|                 (2022, 12), (2022, 9), (2022, 6), (2022, 3), | ||||
|                 (2021, 12), (2021, 9), (2021, 6), (2021, 3), | ||||
|                 (2020, 12), (2020, 9), (2020, 6), (2020, 3) | ||||
|                 (2020, 12), (2020, 9), (2020, 6) | ||||
|             ] | ||||
|              | ||||
|             if i < len(base_quarters): | ||||
|  | @ -538,125 +538,10 @@ class FinancialDataCollectorV2: | |||
|         except Exception as e: | ||||
|             logger.error(f"保存数据到MongoDB失败: {str(e)}") | ||||
|             return False | ||||
|      | ||||
|     def check_missing_data(self, stock_code: str) -> List[str]: | ||||
|         """ | ||||
|         检查MongoDB中哪些报告期的资产负债表或现金流量表数据为空 | ||||
|          | ||||
|         Args: | ||||
|             stock_code: 股票代码 | ||||
|              | ||||
|         Returns: | ||||
|             List[str]: 需要更新的报告期列表 | ||||
|         """ | ||||
|         try: | ||||
|             # 查询该股票的所有记录 | ||||
|             records = list(self.collection.find({'stock_code': stock_code})) | ||||
|              | ||||
|             missing_periods = [] | ||||
|              | ||||
|             for record in records: | ||||
|                 balance_empty = not record.get('balance_sheet') or record.get('balance_sheet') == {} | ||||
|                 cash_empty = not record.get('cash_flow_statement') or record.get('cash_flow_statement') == {} | ||||
|                  | ||||
|                 # 如果资产负债表或现金流量表为空,则需要更新 | ||||
|                 if balance_empty or cash_empty: | ||||
|                     missing_periods.append(record.get('report_date')) | ||||
|                     logger.debug(f"发现需要更新的数据: {stock_code} - {record.get('report_date')} (资产负债表空: {balance_empty}, 现金流量表空: {cash_empty})") | ||||
|              | ||||
|             if missing_periods: | ||||
|                 logger.info(f"股票 {stock_code} 有 {len(missing_periods)} 个报告期需要更新数据") | ||||
|             else: | ||||
|                 logger.info(f"股票 {stock_code} 的数据完整,无需更新") | ||||
|              | ||||
|             return missing_periods | ||||
|              | ||||
|         except Exception as e: | ||||
|             logger.error(f"检查缺失数据失败: {str(e)}") | ||||
|             return [] | ||||
| 
 | ||||
|     def update_missing_financial_data(self, stock_code: str, missing_periods: List[str]) -> bool: | ||||
|         """ | ||||
|         更新缺失的财务数据(只更新资产负债表和现金流量表) | ||||
|          | ||||
|         Args: | ||||
|             stock_code: 股票代码 | ||||
|             missing_periods: 需要更新的报告期列表 | ||||
|              | ||||
|         Returns: | ||||
|             bool: 是否更新成功 | ||||
|         """ | ||||
|         try: | ||||
|             if not missing_periods: | ||||
|                 return True | ||||
|              | ||||
|             logger.info(f"开始更新股票 {stock_code} 缺失的财务数据") | ||||
|              | ||||
|             # 获取资产负债表和现金流量表数据 | ||||
|             balance_data = self.fetch_balance_sheet(stock_code, periods=21) | ||||
|             time.sleep(1) | ||||
|              | ||||
|             cash_data = self.fetch_cash_flow_statement(stock_code, periods=21) | ||||
|             time.sleep(1) | ||||
|              | ||||
|             # 创建按报告日期索引的字典 | ||||
|             balance_dict = {item['REPORT_DATE'][:10]: item for item in balance_data if item.get('REPORT_DATE')} | ||||
|             cash_dict = {item['REPORT_DATE'][:10]: item for item in cash_data if item.get('REPORT_DATE')} | ||||
|              | ||||
|             updated_count = 0 | ||||
|              | ||||
|             for report_date in missing_periods: | ||||
|                 try: | ||||
|                     # 查找当前记录 | ||||
|                     current_record = self.collection.find_one({ | ||||
|                         'stock_code': stock_code, | ||||
|                         'report_date': report_date | ||||
|                     }) | ||||
|                      | ||||
|                     if not current_record: | ||||
|                         logger.warning(f"未找到记录: {stock_code} - {report_date}") | ||||
|                         continue | ||||
|                      | ||||
|                     # 准备更新的字段 | ||||
|                     update_fields = {} | ||||
|                      | ||||
|                     # 检查是否需要更新资产负债表 | ||||
|                     balance_empty = not current_record.get('balance_sheet') or current_record.get('balance_sheet') == {} | ||||
|                     if balance_empty and report_date in balance_dict: | ||||
|                         update_fields['balance_sheet'] = balance_dict[report_date] | ||||
|                         logger.debug(f"更新资产负债表: {stock_code} - {report_date}") | ||||
|                      | ||||
|                     # 检查是否需要更新现金流量表 | ||||
|                     cash_empty = not current_record.get('cash_flow_statement') or current_record.get('cash_flow_statement') == {} | ||||
|                     if cash_empty and report_date in cash_dict: | ||||
|                         update_fields['cash_flow_statement'] = cash_dict[report_date] | ||||
|                         logger.debug(f"更新现金流量表: {stock_code} - {report_date}") | ||||
|                      | ||||
|                     # 如果有字段需要更新 | ||||
|                     if update_fields: | ||||
|                         update_fields['collect_time'] = datetime.datetime.now()  # 更新采集时间 | ||||
|                          | ||||
|                         self.collection.update_one( | ||||
|                             {'stock_code': stock_code, 'report_date': report_date}, | ||||
|                             {'$set': update_fields} | ||||
|                         ) | ||||
|                         updated_count += 1 | ||||
|                         logger.info(f"成功更新: {stock_code} - {report_date}") | ||||
|                      | ||||
|                 except Exception as e: | ||||
|                     logger.error(f"更新记录失败: {stock_code} - {report_date} - {str(e)}") | ||||
|                     continue | ||||
|              | ||||
|             logger.info(f"股票 {stock_code} 更新完成,共更新 {updated_count} 个报告期") | ||||
|             return True | ||||
|              | ||||
|         except Exception as e: | ||||
|             logger.error(f"更新缺失财务数据失败: {str(e)}") | ||||
|             return False | ||||
| 
 | ||||
|     def collect_financial_data(self, stock_code: str, periods: int = 21) -> bool: | ||||
|         """ | ||||
|         采集单只股票的财务数据 - 增量更新模式 | ||||
|         采集单只股票的财务数据 | ||||
|          | ||||
|         Args: | ||||
|             stock_code: 股票代码,如'300750.SZ' | ||||
|  | @ -666,20 +551,37 @@ class FinancialDataCollectorV2: | |||
|             bool: 是否采集成功 | ||||
|         """ | ||||
|         try: | ||||
|             logger.info(f"开始检查股票 {stock_code} 的财务数据") | ||||
|             logger.info(f"开始采集股票 {stock_code} 的财务数据({periods}个报告期)") | ||||
| 
 | ||||
|             # 获取三张财务报表数据 | ||||
|             profit_data = self.fetch_profit_statement(stock_code, periods) | ||||
|             time.sleep(1)  # 避免请求过于频繁 | ||||
| 
 | ||||
|             balance_data = self.fetch_balance_sheet(stock_code, periods) | ||||
|             time.sleep(1) | ||||
| 
 | ||||
|             cash_data = self.fetch_cash_flow_statement(stock_code, periods) | ||||
|             time.sleep(1) | ||||
|              | ||||
|             # 检查哪些报告期的数据缺失 | ||||
|             missing_periods = self.check_missing_data(stock_code) | ||||
|             # 检查至少有一张表有数据 | ||||
|             if not any([profit_data, balance_data, cash_data]): | ||||
|                 logger.error(f"股票 {stock_code} 没有获取到任何财务数据") | ||||
|                 return False | ||||
|              | ||||
|             if not missing_periods: | ||||
|                 logger.info(f"股票 {stock_code} 数据完整,跳过") | ||||
|                 return True | ||||
|             # 处理财务数据 | ||||
|             financial_data_list = self.process_financial_data( | ||||
|                 stock_code, profit_data, balance_data, cash_data | ||||
|             ) | ||||
|              | ||||
|             # 更新缺失的数据 | ||||
|             success = self.update_missing_financial_data(stock_code, missing_periods) | ||||
|             if not financial_data_list: | ||||
|                 logger.error(f"股票 {stock_code} 的财务数据处理失败") | ||||
|                 return False | ||||
| 
 | ||||
|             # 保存到MongoDB | ||||
|             success = self.save_to_mongodb(financial_data_list) | ||||
|              | ||||
|             if success: | ||||
|                 logger.info(f"股票 {stock_code} 的财务数据更新完成") | ||||
|                 logger.info(f"股票 {stock_code} 的财务数据采集完成") | ||||
|              | ||||
|             return success | ||||
|              | ||||
|  | @ -689,19 +591,19 @@ class FinancialDataCollectorV2: | |||
|      | ||||
|     def batch_collect_financial_data(self, stock_codes: List[str], periods: int = 21) -> Dict: | ||||
|         """ | ||||
|         批量更新多只股票的缺失财务数据 | ||||
|         批量采集多只股票的财务数据 | ||||
|          | ||||
|         Args: | ||||
|             stock_codes: 股票代码列表 | ||||
|             periods: 获取多少个报告期,默认21个季度 | ||||
|              | ||||
|         Returns: | ||||
|             Dict: 更新结果统计 | ||||
|             Dict: 采集结果统计 | ||||
|         """ | ||||
|         results = {'success': 0, 'failed': 0, 'failed_stocks': [], 'skipped': 0} | ||||
|         results = {'success': 0, 'failed': 0, 'failed_stocks': []} | ||||
|         total_stocks = len(stock_codes) | ||||
|          | ||||
|         logger.info(f"开始批量检查和更新 {total_stocks} 只股票的财务数据") | ||||
|         logger.info(f"开始批量采集 {total_stocks} 只股票的财务数据") | ||||
|          | ||||
|         for index, stock_code in enumerate(stock_codes, 1): | ||||
|             try: | ||||
|  | @ -712,11 +614,11 @@ class FinancialDataCollectorV2: | |||
|                 success = self.collect_financial_data(stock_code, periods) | ||||
|                 if success: | ||||
|                     results['success'] += 1 | ||||
|                     logger.info(f"SUCCESS [{index}/{total_stocks}] {stock_code} 处理成功") | ||||
|                     logger.info(f"SUCCESS [{index}/{total_stocks}] {stock_code} 采集成功") | ||||
|                 else: | ||||
|                     results['failed'] += 1 | ||||
|                     results['failed_stocks'].append(stock_code) | ||||
|                     logger.warning(f"FAILED [{index}/{total_stocks}] {stock_code} 处理失败") | ||||
|                     logger.warning(f"FAILED [{index}/{total_stocks}] {stock_code} 采集失败") | ||||
|                  | ||||
|                 # 每只股票之间暂停一下,避免请求过于频繁 | ||||
|                 time.sleep(2) | ||||
|  | @ -735,7 +637,7 @@ class FinancialDataCollectorV2: | |||
|                 continue | ||||
|          | ||||
|         success_rate = (results['success'] / total_stocks) * 100 | ||||
|         logger.info(f"批量更新完成: 成功{results['success']}只,失败{results['failed']}只,成功率: {success_rate:.2f}%") | ||||
|         logger.info(f"批量采集完成: 成功{results['success']}只,失败{results['failed']}只,成功率: {success_rate:.2f}%") | ||||
|          | ||||
|         if results['failed_stocks']: | ||||
|             logger.info(f"失败的股票数量: {len(results['failed_stocks'])}") | ||||
|  | @ -757,7 +659,7 @@ class FinancialDataCollectorV2: | |||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     """主函数 - 批量更新所有股票的缺失财务数据""" | ||||
|     """主函数 - 批量采集所有股票的财务数据""" | ||||
|     collector = FinancialDataCollectorV2() | ||||
|      | ||||
|     try: | ||||
|  | @ -771,32 +673,30 @@ def main(): | |||
|          | ||||
|         logger.info(f"从数据库获取到 {len(stock_codes)} 只股票") | ||||
|          | ||||
|         # 可以选择处理所有股票或者部分股票进行测试 | ||||
|         # 可以选择采集所有股票或者部分股票进行测试 | ||||
|         # 如果要测试,可以取前几只股票 | ||||
|         # 测试模式:只处理前10只股票 | ||||
|         TEST_MODE = False  # 设置为False将处理所有股票 | ||||
|         # 测试模式:只采集前10只股票 | ||||
|         TEST_MODE = False  # 设置为False将采集所有股票 | ||||
|          | ||||
|         if TEST_MODE: | ||||
|             test_count = min(10, len(stock_codes))  # 最多取10只股票测试 | ||||
|             stock_codes = stock_codes[:test_count] | ||||
|             logger.info(f"TEST MODE: 仅处理前 {test_count} 只股票") | ||||
|             logger.info(f"TEST MODE: 仅采集前 {test_count} 只股票") | ||||
|         else: | ||||
|             logger.info(f"PRODUCTION MODE: 将处理全部 {len(stock_codes)} 只股票") | ||||
|             logger.info(f"PRODUCTION MODE: 将采集全部 {len(stock_codes)} 只股票") | ||||
|          | ||||
|         logger.info(f"开始批量检查和更新 {len(stock_codes)} 只股票的财务数据") | ||||
|         logger.info("注意: 本次运行为增量更新模式,只会更新缺失的资产负债表和现金流量表数据") | ||||
|         logger.info(f"开始批量采集 {len(stock_codes)} 只股票的财务数据") | ||||
|          | ||||
|         # 批量更新 | ||||
|         # 批量采集 | ||||
|         results = collector.batch_collect_financial_data(stock_codes, periods=21) | ||||
|          | ||||
|         # 输出最终结果 | ||||
|         print(f"\n{'='*50}") | ||||
|         print(f"批量更新完成统计") | ||||
|         print(f"批量采集完成统计") | ||||
|         print(f"{'='*50}") | ||||
|         print(f"SUCCESS 成功处理: {results['success']} 只股票") | ||||
|         print(f"FAILED 处理失败: {results['failed']} 只股票")  | ||||
|         print(f"SUCCESS 成功采集: {results['success']} 只股票") | ||||
|         print(f"FAILED 采集失败: {results['failed']} 只股票") | ||||
|         print(f"SUCCESS RATE 成功率: {(results['success'] / len(stock_codes) * 100):.2f}%") | ||||
|         print(f"\n说明: 成功处理包括数据完整(无需更新)和成功更新缺失数据的股票") | ||||
|          | ||||
|         if results['failed_stocks']: | ||||
|             print(f"\n失败的股票列表:") | ||||
|  | @ -811,7 +711,7 @@ def main(): | |||
|         logger.info("用户中断程序执行") | ||||
|         print("\n警告: 程序被用户中断") | ||||
|     except Exception as e: | ||||
|         logger.error(f"更新过程中出现错误: {str(e)}") | ||||
|         logger.error(f"采集过程中出现错误: {str(e)}") | ||||
|         print(f"\n错误: 程序执行出错: {str(e)}") | ||||
|     finally: | ||||
|         collector.close_connection() | ||||
|  |  | |||
|  | @ -194,24 +194,35 @@ def fetch_and_store_hk_stock_data(page_size=90, max_workers=10, use_proxy=False) | |||
| 
 | ||||
| def format_hk_stock_code(stock_code): | ||||
|     """ | ||||
|     统一港股代码格式,支持0700.HK、HK0700等 | ||||
|     返回雪球格式(如0700.HK)和Redis存储格式 | ||||
|     统一港股代码格式,支持0700.HK、HK0700、00700等 | ||||
|     返回雪球格式(如0700.HK)和Redis存储格式(如00700) | ||||
|     """ | ||||
|     stock_code = stock_code.upper() | ||||
|     if '.HK' in stock_code: | ||||
|         return stock_code, stock_code | ||||
|         code = stock_code.replace('.HK', '') | ||||
|         # 确保是5位数字格式 | ||||
|         if code.isdigit(): | ||||
|             return f'{code.zfill(5)}.HK', code.zfill(5) | ||||
|         else: | ||||
|             return stock_code, code.zfill(5) | ||||
|     elif stock_code.startswith('HK'): | ||||
|         code = stock_code[2:] | ||||
|         return f'{code}.HK', f'{code}.HK' | ||||
|         if code.isdigit(): | ||||
|             return f'{code.zfill(5)}.HK', code.zfill(5) | ||||
|         else: | ||||
|             return stock_code, code.zfill(5) | ||||
|     else: | ||||
|         # 假设是纯数字,添加.HK后缀 | ||||
|         return f'{stock_code}.HK', f'{stock_code}.HK' | ||||
|         # 假设是纯数字,添加.HK后缀并确保5位格式 | ||||
|         if stock_code.isdigit(): | ||||
|             return f'{stock_code.zfill(5)}.HK', stock_code.zfill(5) | ||||
|         else: | ||||
|             return stock_code, stock_code | ||||
| 
 | ||||
| 
 | ||||
| def get_hk_stock_realtime_info_from_redis(stock_code): | ||||
|     """ | ||||
|     根据港股代码从Redis查询实时行情,并封装为指定结构。 | ||||
|     :param stock_code: 支持0700.HK、HK0700等格式 | ||||
|     :param stock_code: 支持0700.HK、HK0700、00700等格式 | ||||
|     :return: dict or None | ||||
|     """ | ||||
|     _, redis_code = format_hk_stock_code(stock_code) | ||||
|  | @ -246,7 +257,7 @@ def get_hk_stock_realtime_info_from_redis(stock_code): | |||
|     result["crawlDate"] = data.get("fetch_time") | ||||
|     result["marketValue"] = data.get("market_capital") | ||||
|     result["maxPrice"] = data.get("high") if "high" in data else data.get("high52w") | ||||
|     result["minPrice"] = data.get("low") if "low" in data else data.get("high52w") | ||||
|     result["minPrice"] = data.get("low") if "low" in data else data.get("low52w") | ||||
|     result["nowPrice"] = data.get("current") | ||||
|     result["pbRate"] = data.get("pb") | ||||
|     result["rangeRiseAndFall"] = data.get("percent") | ||||
|  |  | |||
|  | @ -7,11 +7,10 @@ | |||
| """ | ||||
| 
 | ||||
| import sys | ||||
| import pymongo | ||||
| import pandas as pd | ||||
| import numpy as np | ||||
| import logging | ||||
| from typing import Dict, List, Optional, Tuple | ||||
| from typing import Dict, List, Tuple | ||||
| from pathlib import Path | ||||
| from sqlalchemy import create_engine, text | ||||
| from datetime import datetime | ||||
|  | @ -190,7 +189,7 @@ class TechFundamentalFactorStrategy: | |||
|             logger.info(f"计算 {len(stock_codes)} 只股票的通用因子") | ||||
|              | ||||
|             results = [] | ||||
|             latest_date = "2025-03-31"  # 最新季度数据 | ||||
|             latest_date = "2025-06-30"  # 最新季度数据 | ||||
|             annual_date = "2024-12-31"  # 年报数据 | ||||
|              | ||||
|             for stock_code in stock_codes: | ||||
|  | @ -214,7 +213,7 @@ class TechFundamentalFactorStrategy: | |||
|                     # 3. 前五大供应商占比(使用年报数据) | ||||
|                     supplier_conc = self.financial_analyzer.analyze_supplier_concentration(stock_code, annual_date) | ||||
|                     factor_data['supplier_concentration'] = supplier_conc | ||||
|                      | ||||
| 
 | ||||
|                     # 4. 前五大客户占比(使用年报数据) | ||||
|                     customer_conc = self.financial_analyzer.analyze_customer_concentration(stock_code, annual_date) | ||||
|                     factor_data['customer_concentration'] = customer_conc | ||||
|  | @ -247,7 +246,7 @@ class TechFundamentalFactorStrategy: | |||
|             logger.info(f"计算 {len(stock_codes)} 只成长期股票的特色因子") | ||||
|              | ||||
|             results = [] | ||||
|             latest_date = "2025-03-31"  # 使用最新数据 | ||||
|             latest_date = "2025-06-30"  # 使用最新数据 | ||||
|             annual_date = "2024-12-31"  # 使用年度数据 | ||||
| 
 | ||||
|             for stock_code in stock_codes: | ||||
|  | @ -306,7 +305,7 @@ class TechFundamentalFactorStrategy: | |||
|         try: | ||||
|             logger.info(f"计算 {len(stock_codes)} 只成熟期股票的特色因子") | ||||
|              | ||||
|             latest_date = "2025-03-31"  # 使用最新数据 | ||||
|             latest_date = "2025-06-30"  # 使用最新数据 | ||||
|              | ||||
|             # 在循环外获取全A股PB和ROE数据,避免重复查询 | ||||
|             logger.info("获取全A股PB数据...") | ||||
|  | @ -670,7 +669,7 @@ def main(): | |||
|     try: | ||||
|         print("=== 科技主题基本面因子选股策略 ===") | ||||
|         print("数据说明:") | ||||
|         print("- 毛利率、净利润增长率等:使用最新数据 (2025-03-31)") | ||||
|         print("- 毛利率、净利润增长率等:使用最新数据 (2025-06-30)") | ||||
|         print("- 供应商客户集中度、折旧摊销、研发费用:使用年报数据 (2024-12-31)") | ||||
|         print() | ||||
|          | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ XUEQIU_HEADERS = { | |||
|     'Accept-Encoding': 'gzip, deflate, br, zstd', | ||||
|     'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', | ||||
|     'Client-Version': 'v2.44.75', | ||||
|     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; _c_WBKFRo=dsWgHR8i8KGPbIyhFlN51PHOzVuuNytvUAFppfkD; _nb_ioWEgULi=; Hm_lvt_1db88642e346389874251b5a1eded6e3=1754636834; xq_a_token=4ea8af8f9cb5850af2ba654c5255cbf6bf797b39; xqat=4ea8af8f9cb5850af2ba654c5255cbf6bf797b39; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzU3NjM3OTA4LCJjdG0iOjE3NTUwNDU5MDg0ODksImNpZCI6ImQ5ZDBuNEFadXAifQ.jAeKlW2r1xRuyoZ3cuy2rTgfSoW_79wGJeuup7I7sZMSH5QeRDJrGx5JWXO4373YRpNW0qnAXR51Ygd8Plmko1u99iN8LifGzyMtblXDPgs17aS0zyHr6cMAsURU984wCXkmZxdvRMCHdevc8XWNHnuqeGfQNSgBSdO6Zv7Xc5-t965TJba96UOsNBpv2GghV9B2mcrUQyW3edi9kRAN_Fxmx5M1Iri4Yfppcaj-VSZYkdZtUpizrN5BbVYujcnQjj4kceUYYAl3Ccs273KVNSMFKpHMIOJcMJATY6PRgLvbEu8_ttIfBnbG4mmZ71bU7RXigleXIj1qhcDL2rDzQQ; xq_r_token=2b5db3e3897cb3e46b8fa2fa384471b334ec59cb; acw_tc=ac11000117550489614555169ef3ec63ec008e1bfba0fe5321bc8a30c2deb8; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1755050072; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=kE/XuROkIJ4APDhfxUYOb9lRiDFNJT8KxiXYwJAuoCeNlkaxlcytBSuiCXGjqxhydALLguC/FB4qIXfLut408Q%3D%3D; ssxmod_itna=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuuqxGeR0qxPehAjqDsqze4GzDiLPGhDBWAFdYjw7ivq8RG5pxMBrGHuhLMEzoiqds=iCpxusTh1K/Zjw=KmoeK4xGLDY=DCTKq1QeD4S3Dt4DIDAYDDxDWU4DLDYoDY3nYxGP=xpWTcmRbD0YDzqDgUEe=xi3DA4DjnehqYiTdwDDBDGtO=9aDG4GfSmDD0wDLoGQQoDGWnCneE6mkiFIr6TTDjqPD/Shc59791vGW56CM9zo3paFDtqD90aAFn=GrvFaE_n93e4F4qibH7GYziTmrzt4xmrKi44mBDmAQQ0TKe4Bxq3DPQDhtTH7OY8FYS_Qqx4Gn/lHDcnDd7YerPCYC70bYbC42q3i8WTx3e8/rijIosDPRGrE0WdoqzGh_YPeD; ssxmod_itna2=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuuqxGeR0qxPehAe4DWhYeRonANsR7vNG4g2BxpQTxTiD', | ||||
|     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; _c_WBKFRo=dsWgHR8i8KGPbIyhFlN51PHOzVuuNytvUAFppfkD; _nb_ioWEgULi=; aliyungf_tc=00c6b999835b16cea9e4a6aab36cca373a0976bf55ee74770d11f421f7119ad8; Hm_lvt_1db88642e346389874251b5a1eded6e3=1757464900; xq_a_token=975ab9f15a4965b9e557b9bc6f33bc1da20a0f49; xqat=975ab9f15a4965b9e557b9bc6f33bc1da20a0f49; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzYwMjU2NzM4LCJjdG0iOjE3NTc2NjQ3Mzg4MjUsImNpZCI6ImQ5ZDBuNEFadXAifQ.TMx4-TjKx96j5h6-EGiRIM2WKtJm1xctZhYidc40Em0pRcr0UBHAKBGl3No5r1BElYa9qnEDgNYI0Zv137Inx-EMPqm5cd1Z_ZjLdWOSLzT9qqBj8zdfuqJwP2nCYvC6KLjd8BvykS0vSFKqwb-r0WhEA3OzbO8teVNsaemdKAhBoIyP3-RQCfRxJ9RLNha1ZMdg66iZvfz_SOsG41y8IA9yyl-FFFJOq4TnAiywY1yO1QIJJhkh8YQqfnDfQQdSIFgJGToU980Lw1dm4aCDY-kvn-t18KjrL_hZJ_UNN65bgZsSsuWf-VQ7wsjjczNrfBYAHdZ6kES0CGo9g8IZZw; xq_r_token=c209224335327f29fc555d9910b43c0df6d52d5a; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1757774901; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=rJD0qKtipTMjRBkLBVEyXbl0CiVeY7y4AxEZC0Vf6Zkou9cxp0NPsxwSrnOyFyBMr+Ws5/nJDO1NUalRDyAPsA%3D%3D; acw_tc=3ccdc14717578973700474899e2dc1c35b6358f1af81617250f8f00b4cf31c; ssxmod_itna=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0qRHhqDylAkiETFN1t42Y5D/KlYeDZDGFdDqx0Ei6FiYHK1ezjCGbKSAQY5P53Niio89NQ7DEQm6fjL1S4K7s5h8KRDo9n4hiDB3DbqDymgY5qxGGA4GwDGoD34DiDDPDb8rDALeD7qDFnenropTDm4GWneGfDDoDYbT3xiUYDDUvbeG2iET4DDN4bIGYZ2G76=r1doBip29xKiTDjqPD/ShUoiuzZKC4icFL2/amAeGyC5GuY6mWHQ77SWcbscAV70i8hx_Bx3rKqB5YGDRYqK8o2xY9iKR0YRDxeEDW0DWnQ8EwhDDiP46iRiGDYZtgrNMhXiY4MQA7bAilP4nPkFGCmqzBqGYesQGQiT3ihKbm5CexbxxD; ssxmod_itna2=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0qRHhqDylAkiETFN1t42YeDA4rYnRItORCitz/D3nyGQigbiD', | ||||
|     'Referer': 'https://weibo.com/u/7735765253', | ||||
|     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', | ||||
|     'Sec-Ch-Ua-Mobile': '?0', | ||||
|  |  | |||
|  | @ -0,0 +1,100 @@ | |||
| # coding:utf-8 | ||||
| # 更新港股列表的代码 | ||||
| 
 | ||||
| import requests | ||||
| import pandas as pd | ||||
| from sqlalchemy import create_engine, text | ||||
| import sys | ||||
| import os | ||||
| 
 | ||||
| # 将项目根目录添加到Python路径,以便导入config | ||||
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||||
| from src.scripts.config import XUEQIU_HEADERS | ||||
| 
 | ||||
| def collect_us_stock_codes(db_url): | ||||
|     """ | ||||
|     采集雪球港股列表数据,并存储到数据库。 | ||||
|     """ | ||||
|     engine = create_engine(db_url) | ||||
|     headers = XUEQIU_HEADERS | ||||
|     base_url = "https://stock.xueqiu.com/v5/stock/screener/quote/list.json" | ||||
|     page = 1 | ||||
|     page_size = 90 | ||||
|     all_data = [] | ||||
| 
 | ||||
|     print("--- Starting to collect Hong Kong stock codes ---") | ||||
|      | ||||
|     # 采集前先清空表 | ||||
|     try: | ||||
|         with engine.begin() as conn: | ||||
|             conn.execute(text("TRUNCATE TABLE gp_code_us")) | ||||
|         print("Table `gp_code_us` has been truncated.") | ||||
|     except Exception as e: | ||||
|         print(f"Error truncating table `gp_code_us`: {e}") | ||||
|         return | ||||
| 
 | ||||
|     while True: | ||||
|         params = { | ||||
|             'page': page, | ||||
|             'size': page_size, | ||||
|             'order': 'desc', | ||||
|             'order_by': 'market_capital', | ||||
|             'market': 'US', | ||||
|             'type': 'us', | ||||
|             'is_delay': 'true' | ||||
|         } | ||||
|          | ||||
|         print(f"Fetching page {page}...") | ||||
|         try: | ||||
|             response = requests.get(base_url, headers=headers, params=params, timeout=20) | ||||
|             if response.status_code != 200: | ||||
|                 print(f"Request failed with status code {response.status_code}") | ||||
|                 break | ||||
|              | ||||
|             data = response.json() | ||||
|             if data.get('error_code') != 0: | ||||
|                 print(f"API error: {data.get('error_description')}") | ||||
|                 break | ||||
| 
 | ||||
|             stock_list = data.get('data', {}).get('list', []) | ||||
|             if not stock_list: | ||||
|                 print("No more data found. Collection finished.") | ||||
|                 break | ||||
| 
 | ||||
|             all_data.extend(stock_list) | ||||
|              | ||||
|             # 如果获取到的数据少于每页数量,说明是最后一页 | ||||
|             if len(stock_list) < page_size: | ||||
|                 print("Reached the last page. Collection finished.") | ||||
|                 break | ||||
|              | ||||
|             page += 1 | ||||
| 
 | ||||
|         except requests.exceptions.RequestException as e: | ||||
|             print(f"Request exception on page {page}: {e}") | ||||
|             break | ||||
|      | ||||
|     if all_data: | ||||
|         print(f"--- Collected a total of {len(all_data)} stocks. Preparing to save to database. ---") | ||||
|         df = pd.DataFrame(all_data) | ||||
|          | ||||
|         # 数据映射和转换 | ||||
|         df_to_save = pd.DataFrame() | ||||
|         df_to_save['gp_name'] = df['name'] | ||||
|         df_to_save['gp_code'] = df['symbol'] | ||||
|         df_to_save['gp_code_two'] = 'US.' + df['symbol'].astype(str) | ||||
|         df_to_save['market_cap'] = df['market_capital'] | ||||
|          | ||||
|         try: | ||||
|             df_to_save.to_sql('gp_code_us', engine, if_exists='append', index=False) | ||||
|             print("--- Successfully saved all data to `gp_code_us`. ---") | ||||
|         except Exception as e: | ||||
|             print(f"Error saving data to database: {e}") | ||||
|     else: | ||||
|         print("--- No data collected. ---") | ||||
|      | ||||
|     engine.dispose() | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj' | ||||
|     collect_us_stock_codes(db_url) | ||||
							
								
								
									
										118
									
								
								src/temp.csv
								
								
								
								
							
							
						
						
									
										118
									
								
								src/temp.csv
								
								
								
								
							|  | @ -1,118 +0,0 @@ | |||
| 20230508_20240508下跌后快速反弹走势,,,,,,,, | ||||
| take_profit_pct,avg_profit_per_trade,median_profit_per_trade,avg_holding_days,trade_win_rate,stock_win_rate,total_trades,stock_count, | ||||
| 30,-4712,0,100.8,49.8,53.1,1815,405, | ||||
| 29,-5206,0,100,49.9,53.3,1818,405, | ||||
| 28,-6335,0,99.2,50,53.6,1822,405, | ||||
| 27,-6590,0,98,50.2,53.6,1827,405, | ||||
| 26,-6865,0,96.7,50.5,54.1,1833,405, | ||||
| 25,-6648,0,95.4,51,54.3,1836,405, | ||||
| 24,-6931,0,93.9,51.4,54.1,1835,403, | ||||
| 23,-5709,0,91.9,52.2,54.5,1837,404, | ||||
| 22,-6556,0,90.7,52.4,54.6,1852,405, | ||||
| 21,-5242,0,88.3,53.1,55.3,1858,405, | ||||
| 20,-5913,0,86.6,53.7,55.3,1866,405, | ||||
| 19,-5848,0,84.1,54.5,55.6,1876,405, | ||||
| 18,-4698,0,81.5,55.6,56.3,1882,405, | ||||
| 17,-5508,0,79.3,56.4,56.3,1888,405, | ||||
| 16,-4942,0,76.6,57.5,57,1897,405, | ||||
| 15,-5598,0,74.1,58.6,58.5,1903,405, | ||||
| 14,-6530,0,71,59.6,58.5,1910,405, | ||||
| 13,-6346,0,68.3,61,58.8,1919,405, | ||||
| 12,-6840,0,65.3,62.6,58.8,1926,405, | ||||
| 11,-7236,0,62.5,64.4,58.5,1931,405, | ||||
| 10,-7611,0,59.3,66.1,58.8,1939,405, | ||||
| 9,-9419,0,55.9,67.5,59,1949,405, | ||||
| 8,-9855,0,52.7,69.4,59,1958,405, | ||||
| 7,-10832,0,49.2,71.4,58.8,1969,405, | ||||
| 6,-11762,0,45.3,73.7,59.8,1973,405, | ||||
| 5,-13031,0,41.6,75.9,60.5,1978,405, | ||||
| ,,,,,,,, | ||||
| ,,,,,,,, | ||||
| 20230508_20240205下跌走势,,,,,,,, | ||||
| take_profit_pct,avg_profit_per_trade,median_profit_per_trade,avg_holding_days,trade_win_rate,stock_win_rate,total_trades,stock_count, | ||||
| 30,-120990,0,75.6,10.9,8.8,1465,396, | ||||
| 29,-120762,0,75.4,10.9,8.6,1466,396, | ||||
| 28,-120539,0,75.1,11.1,8.3,1468,396, | ||||
| 27,-119956,0,74.8,11.4,9.1,1469,396, | ||||
| 26,-118942,0,74.3,11.6,9.6,1471,396, | ||||
| 25,-117868,0,73.8,11.9,9.3,1471,396, | ||||
| 24,-117173,0,73.3,12.3,9.3,1471,396, | ||||
| 23,-114536,0,72.2,13,9.6,1474,396, | ||||
| 22,-113484,0,71.5,13.3,9.8,1479,396, | ||||
| 21,-109829,0,69.9,14.4,10.9,1482,396, | ||||
| 20,-108867,0,69,15,10.9,1486,396, | ||||
| 19,-106925,0,67.9,15.8,11.4,1492,396, | ||||
| 18,-104300,0,66.7,16.8,11.4,1496,396, | ||||
| 17,-102194,0,65.5,17.7,12.6,1499,396, | ||||
| 16,-99208,0,64,18.9,12.9,1506,396, | ||||
| 15,-97766,0,62.6,19.8,13.1,1511,396, | ||||
| 14,-96754,0,61.2,20.6,13.1,1515,396, | ||||
| 13,-93449,0,59.5,22.3,13.9,1522,396, | ||||
| 12,-91252,0,57.7,23.9,14.6,1526,396, | ||||
| 11,-88804,0,56.1,25.3,15.2,1527,396, | ||||
| 10,-84415,0,53.6,27.9,17.4,1535,396, | ||||
| 9,-80819,0,50.9,30.7,19.7,1543,396, | ||||
| 8,-77402,0,48.2,33.2,21.5,1547,396, | ||||
| 7,-72609,0,45.1,36.2,23.5,1556,396, | ||||
| 6,-67130,0,41.5,40.1,27,1559,396, | ||||
| 5,-63089,0,38.3,43.3,27,1563,396, | ||||
| ,,,,,,,, | ||||
| ,,,,,,,, | ||||
| 20221028_20230508上涨走势,,,,,,,, | ||||
| take_profit_pct,avg_profit_per_trade,median_profit_per_trade,avg_holding_days,trade_win_rate,stock_win_rate,total_trades,stock_count, | ||||
| 30,-26094,0,84.9,35.4,39.4,1022,343, | ||||
| 29,-24312,0,83.6,36.2,40.2,1027,343, | ||||
| 28,-24443,0,82.7,36.7,40.5,1031,343, | ||||
| 27,-23021,0,81.2,37.2,41.1,1034,343, | ||||
| 26,-22763,0,80,37.7,41.4,1036,343, | ||||
| 25,-21882,0,78,38.5,42,1040,343, | ||||
| 24,-22570,0,77.1,38.9,41.4,1042,343, | ||||
| 23,-22085,0,75.5,39.6,42,1048,343, | ||||
| 22,-21214,0,74.1,40.4,43.4,1053,343, | ||||
| 21,-20730,0,72.7,41.3,44,1059,343, | ||||
| 20,-18191,0,70.6,42.5,44.6,1064,343, | ||||
| 19,-17678,0,68.8,43.4,43.7,1068,343, | ||||
| 18,-18481,0,67.2,44,43.1,1072,343, | ||||
| 17,-16782,0,64.9,45.4,44.9,1076,343, | ||||
| 16,-16490,0,62.9,46.4,45.8,1081,343, | ||||
| 15,-15946,0,60.4,47.7,46.4,1089,343, | ||||
| 14,-16037,0,58.4,48.9,47.2,1095,343, | ||||
| 13,-16701,0,56.4,49.8,48.1,1100,343, | ||||
| 12,-16062,0,54,51.3,49,1106,343, | ||||
| 11,-16000,0,51.4,52.9,49.6,1110,343, | ||||
| 10,-15453,0,49.1,54.4,49.6,1114,343, | ||||
| 9,-14841,0,46.2,56.2,49.9,1121,343, | ||||
| 8,-14211,0,43,58.3,51.3,1132,343, | ||||
| 7,-14199,0,39.9,60.5,53.9,1138,343, | ||||
| 6,-15366,0,37.3,62.4,52.8,1144,343, | ||||
| 5,-15711,0,34.1,64.9,50.7,1152,343, | ||||
| ,,,,,,,, | ||||
| ,,,,,,,, | ||||
| 20210106_20220224震荡走势,,,,,,,, | ||||
| take_profit_pct,avg_profit_per_trade,median_profit_per_trade,avg_holding_days,trade_win_rate,stock_win_rate,total_trades,stock_count, | ||||
| 30,12047,0,115.6,44.5,48.8,2508,424, | ||||
| 29,12857,0,113.5,45.1,48.5,2525,425, | ||||
| 28,13349,0,112,45.7,48.9,2538,425, | ||||
| 27,12644,0,110.5,46.3,49.4,2554,425, | ||||
| 26,12878,0,108.4,47.1,48.7,2571,425, | ||||
| 25,12319,0,106.9,47.7,48.5,2589,425, | ||||
| 24,12744,0,104.5,48.6,50.4,2608,425, | ||||
| 23,11949,0,102.3,49.4,51.3,2626,425, | ||||
| 22,11246,0,100.6,50.3,51.3,2637,425, | ||||
| 21,10399,0,98.7,51.1,51.1,2644,425, | ||||
| 20,9908,0,96.3,52,51.1,2666,425, | ||||
| 19,9327,0,93.8,53,52.5,2681,425, | ||||
| 18,7509,0,92.1,53.6,52.6,2686,424, | ||||
| 17,7333,0,89.4,55.1,53.9,2713,425, | ||||
| 16,6404,0,86.6,56.2,54.8,2745,425, | ||||
| 15,5296,0,83.7,57.3,55.3,2763,425, | ||||
| 14,5382,0,80.5,59,54.6,2788,425, | ||||
| 13,4527,0,77.7,60.4,56.6,2800,424, | ||||
| 12,3650,0,74.3,62,57.2,2826,425, | ||||
| 11,1583,0,71.1,63.3,57.2,2849,425, | ||||
| 10,-741,0,68.1,64.7,56.9,2859,425, | ||||
| 9,-2581,0,64.5,66.3,56.9,2879,425, | ||||
| 8,-3833,0,60.7,68.3,56.7,2902,425, | ||||
| 7,-4992,0,56.7,70.5,57.9,2924,425, | ||||
| 6,-4815,0,52.1,73.6,60.5,2948,425, | ||||
| 5,-7635,0,47.9,75.6,58.8,2965,425, | ||||
| 
 | 
		Loading…
	
		Reference in New Issue