From 2f6fd15e4ef8f429ae38870aca2a94bb2655402e Mon Sep 17 00:00:00 2001 From: Junchao Date: Wed, 19 Nov 2025 23:18:58 +0800 Subject: [PATCH 01/16] little fix --- delegate/gm_callback.py | 54 +++++++++++++++++++++++------------------ trader/pools.py | 11 +++++++-- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/delegate/gm_callback.py b/delegate/gm_callback.py index f417a5f..a032b6f 100644 --- a/delegate/gm_callback.py +++ b/delegate/gm_callback.py @@ -45,33 +45,33 @@ def register_callback(): try: status = start(filename=file_name) if status == 0: - print(f'[掘金]:使用{file_name}订阅回调成功') + print(f'[掘金信息]使用{file_name}订阅回调成功') else: - print(f'[掘金]:使用{file_name}订阅回调失败,状态码:{status}') + print(f'[掘金信息]使用{file_name}订阅回调失败,状态码:{status}') except Exception as e0: - print(f'[掘金]:使用{file_name}订阅回调异常:{e0}') + print(f'[掘金信息]使用{file_name}订阅回调异常:{e0}') try: # 直接使用当前模块进行注册,不使用filename参数 status = start(filename='__main__') if status == 0: - print(f'[掘金]:使用__main__订阅回调成功') + print(f'[掘金信息]使用__main__订阅回调成功') else: - print(f'[掘金]:使用__main__订阅回调失败,状态码:{status}') + print(f'[掘金信息]使用__main__订阅回调失败,状态码:{status}') except Exception as e1: - print(f'[掘金]:使用__main__订阅回调异常:{e1}') + print(f'[掘金信息]使用__main__订阅回调异常:{e1}') try: # 如果start()不带参数失败,尝试使用空参数 status = start() if status == 0: - print(f'[掘金]:订阅回调成功') + print(f'[掘金信息]订阅回调成功') else: - print(f'[掘金]:订阅回调失败,状态码:{status}') + print(f'[掘金信息]订阅回调失败,状态码:{status}') except Exception as e2: - print(f'[掘金]:使用空参数订阅回调也失败:{e2}') + print(f'[掘金信息]使用空参数订阅回调也失败:{e2}') @staticmethod def unregister_callback(): - print(f'[掘金]:取消订阅回调') + print(f'[掘金信息]取消订阅回调') # stop() def record_order(self, order_time: str, code: str, price: float, volume: int, side: str, remark: str): @@ -137,33 +137,39 @@ def on_order_status(self, order: Order): f'{datetime.datetime.now().strftime("%H:%M:%S")} 买成 {stock_code}{MSG_OUTER_SEPARATOR}' f'{name} {traded_volume}股 {traded_price:.2f}元', '[BOUGHT]') - + elif order.status == OrderStatus_New: + print(f'[NEW:{order.symbol}]', end='') + elif order.status == OrderStatus_Canceled: + print(f'[CANCELED:{order.symbol}]', end='') else: - print(order.status, order.symbol) + print(f'[掘金订单]{order.symbol} 订单状态:{order.status}') class GmCache: gm_callback: Optional[GmCallback] = None +def on_execution_report(rpt: ExecRpt): + # print('[掘金回调]:成交状态已变化') + GmCache.gm_callback.on_execution_report(rpt) + + +def on_order_status(order: Order): + # print('[掘金回调]:订单状态已变') + GmCache.gm_callback.on_order_status(order) + + def on_trade_data_connected(): - print('[掘金回调]:交易服务已连接') + print('[掘金回调]交易服务已连接') def on_trade_data_disconnected(): - print('\n[掘金回调]:交易服务已断开') + print('[掘金回调]交易服务已断开') def on_account_status(account_status: AccountStatus): - print('[掘金回调]:账户状态已变化') - print(f'on_account_status status={account_status}') - + print(f'[掘金回调]账户状态已变化 状态:{account_status}') -def on_execution_report(rpt: ExecRpt): - # print('[掘金回调]:成交状态已变化') - GmCache.gm_callback.on_execution_report(rpt) - -def on_order_status(order: Order): - # print('[掘金回调]:订单状态已变') - GmCache.gm_callback.on_order_status(order) +def on_error(error_code, error_info): + print(f'[掘金报错]错误码:{error_code} 错误信息:{error_info}') diff --git a/trader/pools.py b/trader/pools.py index 8277288..00ca6bc 100644 --- a/trader/pools.py +++ b/trader/pools.py @@ -26,10 +26,16 @@ def __init__(self, account_id: str, strategy_name: str, parameters: any, ding_me def get_code_list(self) -> list[str]: return self.cache_code_list + def update_code_list(self) -> None: + self.cache_code_list = list(self.cache_whitelist.difference(self.cache_blacklist)) + + def clear_code_list(self) -> None: + self.cache_code_list = [] + def refresh(self): self.refresh_black() self.refresh_white() - self.cache_code_list = list(self.cache_whitelist.difference(self.cache_blacklist)) + self.update_code_list() print(f'[POOL] White list refreshed {len(self.cache_whitelist)} codes.') print(f'[POOL] Black list refreshed {len(self.cache_blacklist)} codes.') @@ -70,7 +76,8 @@ def filter_white_list_by_selector(self, filter_func: Callable, cache_history: di for code in remove_list: self.cache_whitelist.discard(code) - print(f'[POOL] {len(remove_list)} codes filter out.') + self.update_code_list() + print(f'[POOL] {len(remove_list)} codes filter out, {len(self.get_code_list())} codes left.') if self.messager is not None: self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:筛除{len(remove_list)}支') From c986e690eee49375e1667f4125a936ce9427bebc Mon Sep 17 00:00:00 2001 From: dominicx Date: Tue, 25 Nov 2025 15:08:09 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E6=88=90=E6=9C=AC=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=98=BE=E7=A4=BA3=E4=BD=8D=E5=B0=8F=E6=95=B0=EF=BC=8C?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E4=B8=8Eapp=E4=B8=80=E8=87=B4=E4=B8=94?= =?UTF-8?q?=E5=85=BC=E5=AE=B9ETF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- delegate/daily_reporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delegate/daily_reporter.py b/delegate/daily_reporter.py index 81630b9..d4b9e66 100644 --- a/delegate/daily_reporter.py +++ b/delegate/daily_reporter.py @@ -137,7 +137,7 @@ def today_hold_report(self, today: str, positions): f'{self.stock_names.get_name(code)} ' \ f'{curr_price * vol:.2f}元' text += MSG_INNER_SEPARATOR - text += f'成本 {open_price:.2f} 盈亏 {total_change} ({ratio_change})' + text += f'成本 {open_price:.3f} 盈亏 {total_change} ({ratio_change})' title = f'[{self.account_id}]{self.strategy_name} 持仓统计' text = f'{title}\n\n[{today}] 持仓{hold_count}支\n{text}' From 4995a97a51ebeef8877682aaadd05b6b74b53a4e Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:55:18 +0800 Subject: [PATCH 03/16] Fix baostock volume unit not alignment --- run_redis_pull.py | 2 +- run_swords.py | 8 +------- tools/utils_remote.py | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/run_redis_pull.py b/run_redis_pull.py index a2c5e6a..e33ab42 100644 --- a/run_redis_pull.py +++ b/run_redis_pull.py @@ -172,7 +172,7 @@ def scan_sell(quotes: Dict, curr_date: str, curr_time: str, positions: List) -> def empty_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quotes: Dict) -> bool: # 这里的时间是本机时间,redis listener 的时间是数据推送服务器时间,需要注意时间不对齐的问题 - return False + return curr_date is None and curr_time is None and curr_seconds is None and curr_quotes is None def redis_subscriber(): diff --git a/run_swords.py b/run_swords.py index beca79c..848739e 100644 --- a/run_swords.py +++ b/run_swords.py @@ -172,13 +172,7 @@ def select_stocks( return selections -def scan_buy( - quotes: Dict, - curr_date: str, - curr_time: str, - curr_seconds: str, - positions: List, -) -> None: +def scan_buy(quotes: Dict, curr_date: str, curr_time: str, curr_seconds: str, positions: List) -> None: selections = select_stocks(quotes, curr_time, curr_seconds) # debug(f'本次扫描:{len(quotes)}, 选股{selections}) diff --git a/tools/utils_remote.py b/tools/utils_remote.py index 8142f86..afc7fb9 100644 --- a/tools/utils_remote.py +++ b/tools/utils_remote.py @@ -503,7 +503,7 @@ def get_bao_daily_history( df['high'] = df['high'].astype(float) df['low'] = df['low'].astype(float) df['close'] = df['close'].astype(float) - df['volume'] = df['volume'].astype(int) + df['volume'] = df['volume'].astype(int) / 100 df['amount'] = df['amount'].astype(float) if df is not None and len(df) > 0: From 614dd0371d670d49d56be6ef5be0c564d76e153f Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:33:43 +0800 Subject: [PATCH 04/16] Update utils_remote.py --- tools/utils_remote.py | 126 +++++++++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 46 deletions(-) diff --git a/tools/utils_remote.py b/tools/utils_remote.py index afc7fb9..4ed85d5 100644 --- a/tools/utils_remote.py +++ b/tools/utils_remote.py @@ -2,6 +2,8 @@ import csv import requests +import threading +import atexit from typing import Optional from tools.constants import DataSource, ExitRight, DEFAULT_DAILY_COLUMNS @@ -10,6 +12,46 @@ from tools.utils_mootdx import MootdxClientInstance, get_mootdx_daily_history +class BaoStockInstance: + _instance = None + _lock = threading.Lock() + bs = None + _initialized = False + _login_ok = False + + def __new__(cls): + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super(BaoStockInstance, cls).__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + import baostock as bs + self.bs = bs + print('[BAOSTOCK] login...', end='') + lg = bs.login() + if lg.error_code == '0': + self._login_ok = True + else: + print('[BAOSTOCK] login respond error_msg: ' + lg.error_msg) + atexit.register(self.logout) + self._initialized = True + + def logout(self): + if self.bs is not None and self._login_ok: + try: + print('[BAOSTOCK] logout...', end='') + self.bs.logout() + except Exception as e: + print('[BAOSTOCK] logout error!', e) + self._login_ok = False + + def __del__(self): + self.logout() + + def set_tdx_zxg_code(data: list[str], file_name: str = None, block_name: str = '自选股') -> None: if file_name is None: try: @@ -468,54 +510,46 @@ def get_bao_daily_history( start = f"{str(start_date)[:4]}-{str(start_date)[4:6]}-{str(start_date)[6:]}" end = f"{str(end_date)[:4]}-{str(end_date)[4:6]}-{str(end_date)[6:]}" - import baostock as bs - lg = bs.login() - - if lg.error_code == '0': - # 1:后复权, 2:前复权,3:不复权 - adjust_flag = '3' - if adjust == ExitRight.QFQ: - adjust_flag = '2' - elif adjust == ExitRight.HFQ: - adjust_flag = '1' - - [symbol, exchange] = code.split('.') - rs = bs.query_history_k_data_plus( - f'{exchange.lower()}.{symbol}', - "date,code,open,high,low,close,volume,amount", - start_date=start, - end_date=end, - frequency='d', - adjustflag=adjust_flag, - ) - if rs.error_code == '0': - data_list = [] - while (rs.error_code == '0') & rs.next(): - data_list.append(rs.get_row_data()) - - bs.logout() - - df = pd.DataFrame(data_list, columns=rs.fields) - df['date'] = pd.to_datetime(df['date']).dt.strftime('%Y%m%d').astype(int) - df = df.rename(columns={'date': 'datetime'}) - df['datetime'] = df['datetime'].astype(int) - df['open'] = df['open'].astype(float) - df['high'] = df['high'].astype(float) - df['low'] = df['low'].astype(float) - df['close'] = df['close'].astype(float) - df['volume'] = df['volume'].astype(int) / 100 - df['amount'] = df['amount'].astype(float) - - if df is not None and len(df) > 0: - if columns is not None: - return df[columns] - return df - return None - else: - print(f'query_history_k_data_plus {code} respond error_msg:' + rs.error_msg) + bs = BaoStockInstance().bs + + adjust_flag = '3' + if adjust == ExitRight.QFQ: + adjust_flag = '2' + elif adjust == ExitRight.HFQ: + adjust_flag = '1' + + [symbol, exchange] = code.split('.') + rs = bs.query_history_k_data_plus( + f'{exchange.lower()}.{symbol}', + "date,code,open,high,low,close,volume,amount", + start_date=start, + end_date=end, + frequency='d', + adjustflag=adjust_flag, + ) + if rs.error_code == '0': + data_list = [] + while (rs.error_code == '0') & rs.next(): + data_list.append(rs.get_row_data()) + + df = pd.DataFrame(data_list, columns=rs.fields) + df['date'] = pd.to_datetime(df['date']).dt.strftime('%Y%m%d').astype(int) + df = df.rename(columns={'date': 'datetime'}) + df['datetime'] = df['datetime'].astype(int) + df['open'] = df['open'].astype(float) + df['high'] = df['high'].astype(float) + df['low'] = df['low'].astype(float) + df['close'] = df['close'].astype(float) + df['volume'] = df['volume'].astype(int) / 100 + df['amount'] = df['amount'].astype(float) + + if df is not None and len(df) > 0: + if columns is not None: + return df[columns] + return df return None else: - print('login respond error_msg:' + lg.error_msg) + print(f'query_history_k_data_plus {code} respond error_msg:' + rs.error_msg) return None From 6073ebf23739fcd93b7ad2c978a191df462729ce Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:06:55 +0800 Subject: [PATCH 05/16] refactor subscriber, split into 3 parts --- delegate/base_delegate.py | 35 +++ delegate/base_subscriber.py | 457 ++++++++++++++++++++++++++++++++++ delegate/daily_reporter.py | 7 +- delegate/xt_subscriber.py | 483 +++++------------------------------- run_ai_gen.py | 3 +- run_redis_pull.py | 7 +- run_remote.py | 3 +- run_shield.py | 14 +- run_swords.py | 16 +- run_wencai_qmt.py | 14 +- run_wencai_tdx.py | 7 +- trader/seller_components.py | 6 +- 12 files changed, 598 insertions(+), 454 deletions(-) create mode 100644 delegate/base_subscriber.py diff --git a/delegate/base_delegate.py b/delegate/base_delegate.py index e178336..40a2266 100644 --- a/delegate/base_delegate.py +++ b/delegate/base_delegate.py @@ -1,4 +1,7 @@ +import threading + from abc import ABC, abstractmethod +from tools.utils_cache import InfoItem, load_json, save_json DEFAULT_STRATEGY_NAME = '空白策略' @@ -83,3 +86,35 @@ def is_position_holding(position: any) -> bool: @abstractmethod def get_holding_position_count(self, positions: list, only_stock: bool = False) -> int: return 0 + + @abstractmethod + def shutdown(self) -> None: + pass + + +# ----------------------- +# 持仓自动发现 +# ----------------------- +def update_position_held(lock: threading.Lock, delegate: BaseDelegate, path: str): + with lock: + positions = delegate.check_positions() + held_info = load_json(path) + + # 添加未被缓存记录的持仓:默认当日买入 + for position in positions: + if position.can_use_volume > 0: + if position.stock_code not in held_info.keys(): + held_info[position.stock_code] = {InfoItem.DayCount: 0} + + # 删除已清仓的held_info记录 + if positions is not None and len(positions) > 0: + position_codes = [position.stock_code for position in positions] + print('[当前持仓]', position_codes) + holding_codes = list(held_info.keys()) + for code in holding_codes: + if len(code) > 0 and code[0] != '_' and (code not in position_codes): + del held_info[code] + else: + print('[当前空仓]') + + save_json(path, held_info) diff --git a/delegate/base_subscriber.py b/delegate/base_subscriber.py new file mode 100644 index 0000000..800df2e --- /dev/null +++ b/delegate/base_subscriber.py @@ -0,0 +1,457 @@ +import time +import datetime +import random +import threading +import traceback +from typing import Dict, Callable, Optional + +import pandas as pd + +from delegate.base_delegate import BaseDelegate +from delegate.daily_reporter import DailyReporter +from delegate.daily_history import DailyHistoryCache + +from tools.utils_cache import StockNames, check_is_open_day, load_pickle, save_pickle, get_trading_date_list +from tools.utils_ding import BaseMessager +from tools.utils_mootdx import get_tdxzip_history +from tools.utils_remote import DataSource, ExitRight, get_daily_history + + +class BaseSubscriber: + def __init__( + self, + # 基本信息 + account_id: str, + delegate: Optional[BaseDelegate], + strategy_name: str, + path_deal: str, + path_assets: str, + # 回调 + execute_strategy: Callable, # 策略回调函数 + execute_interval: int = 1, # 策略执行间隔,单位(秒) + before_trade_day: Callable = None, # 盘前函数 + near_trade_begin: Callable = None, # 盘后函数 + finish_trade_day: Callable = None, # 盘后函数 + # 非QMT + custom_sub_begin: Callable = None, # 使用外部数据时的自定义启动 + custom_unsub_end: Callable = None, # 使用外部数据时的自定义关闭 + # 通知 + ding_messager: BaseMessager = None, + # 日报 + open_today_deal_report: bool = False, # 每日交易记录报告 + open_today_hold_report: bool = False, # 每日持仓记录报告 + today_report_show_bank: bool = False, # 是否显示银行流水(国金QMT会卡死所以默认关闭) + ): + self.account_id = '**' + str(account_id)[-4:] + self.delegate = delegate + if self.delegate is not None: + self.delegate.subscriber = self + + self.strategy_name = strategy_name + self.path_deal = path_deal + self.path_assets = path_assets + + self.execute_strategy = execute_strategy + self.execute_interval = execute_interval + self.before_trade_day = before_trade_day # 提前准备某些耗时长的任务 + self.near_trade_begin = near_trade_begin # 有些数据临近开盘才更新,这里保证内存里的数据正确 + self.finish_trade_day = finish_trade_day # 盘后及时做一些总结汇报入库类的整理工作 + + self.custom_begin_sub = custom_sub_begin + self.custom_end_unsub = custom_unsub_end + + self.open_today_deal_report = open_today_deal_report + self.open_today_hold_report = open_today_hold_report + self.today_report_show_bank = today_report_show_bank + + self.messager = ding_messager + + self.scheduler = None + self.use_ap_scheduler = True # 如果use_outside_data 被设置为True,则需强制使用apscheduler + self.create_scheduler() + + self.stock_names = StockNames() + self.daily_reporter = DailyReporter( + account_id=self.account_id, + delegate=self.delegate, + strategy_name=self.strategy_name, + path_deal=self.path_deal, + path_assets=self.path_assets, + messager=self.messager, + use_outside_data=True, + today_report_show_bank=self.today_report_show_bank, + ) + + self.cache_limits: Dict[str, str] = { # 限制执行次数的缓存集合 + 'prev_seconds': '', # 限制每秒一次跑策略扫描的缓存 + 'prev_minutes': '', # 限制每分钟屏幕心跳换行的缓存 + } + self.cache_history: Dict[str, pd.DataFrame] = {} # 记录历史日线行情的信息 { code: DataFrame } + + # 这个成员变量区别于cache_history,保存全部股票的日线数据550天,cache_history只包含code_list中指定天数数据 + self.history_day_klines : Dict[str, pd.DataFrame] = {} + + self.code_list = [] + self.curr_trade_date = '' + + # ----------------------- + # 监测主策略执行 + # ----------------------- + def callback_run_no_quotes(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + now = datetime.datetime.now() + + curr_date = now.strftime('%Y-%m-%d') + curr_time = now.strftime('%H:%M') + + # 每分钟输出一行开头 + if self.cache_limits['prev_minutes'] != curr_time: + self.cache_limits['prev_minutes'] = curr_time + print(f'\n[{curr_time}]', end='') + + curr_seconds = now.strftime('%S') + if self.cache_limits['prev_seconds'] != curr_seconds: + self.cache_limits['prev_seconds'] = curr_seconds + + if int(curr_seconds) % self.execute_interval == 0: + print('.', end='') # cache_quotes 肯定没数据,这里就是输出观察线程健康 + # str(%Y-%m-%d), str(%H:%M), str(%S) + self.execute_strategy(curr_date, curr_time, curr_seconds, {}) + + def callback_open_no_quotes(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + print('[启动策略]', end='') + + if self.messager is not None: + self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:开启') + + if self.custom_begin_sub is not None: + threading.Thread(target=self.custom_begin_sub).start() + + def callback_close_no_quotes(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + if self.custom_end_unsub is not None: + threading.Thread(target=self.custom_end_unsub).start() + + if self.messager is not None: + self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:结束') + + print('\n[关闭策略]') + + def update_code_list(self, code_list: list[str]): + self.code_list = code_list + + # ----------------------- + # 任务接口 + # ----------------------- + def before_trade_day_wrapper(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + if self.before_trade_day is not None: + self.before_trade_day() + self.curr_trade_date = datetime.datetime.now().strftime('%Y-%m-%d') + + def near_trade_begin_wrapper(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + if self.near_trade_begin is not None: + self.near_trade_begin() + if self.before_trade_day is None: # 没有设置before_trade_day 情况 + self.curr_trade_date = datetime.datetime.now().strftime('%Y-%m-%d') + print(f'今日盘前准备工作已完成') + + def finish_trade_day_wrapper(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + if self.finish_trade_day is not None: + self.finish_trade_day() + + # 检查是否完成盘前准备 + def check_before_finished(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + if (self.before_trade_day is not None or self.near_trade_begin is not None) and \ + (self.curr_trade_date != datetime.datetime.now().strftime("%Y-%m-%d")): + print('[ERROR]盘前准备未完成,尝试重新执行盘前函数') + self.before_trade_day_wrapper() + self.near_trade_begin_wrapper() + print(f'当前交易日:[{self.curr_trade_date}]') + + # ----------------------- + # 盘后报告总结 + # ----------------------- + def daily_summary(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + curr_date = datetime.datetime.now().strftime('%Y-%m-%d') + + if self.open_today_deal_report: + try: + self.daily_reporter.today_deal_report(today=curr_date) + except Exception as e: + print('Report deal failed: ', e) + traceback.print_exc() + + if self.open_today_hold_report: + try: + if self.delegate is not None: + positions = self.delegate.check_positions() + self.daily_reporter.today_hold_report(today=curr_date, positions=positions) + else: + print('Missing delegate to complete reporting!') + except Exception as e: + print('Report position failed: ', e) + traceback.print_exc() + + try: + if self.delegate is not None: + asset = self.delegate.check_asset() + self.daily_reporter.check_asset(today=curr_date, asset=asset) + except Exception as e: + print('Report asset failed: ', e) + traceback.print_exc() + + # ----------------------- + # 定时器 + # ----------------------- + def _start_scheduler(self): + run_time_ranges = [ + # 上午时间段: 09:15:00 到 11:29:59 + { + 'hour': '9', + 'minute': '15-59', + 'second': '0-59' # 9:15到9:59的每秒 + }, + { + 'hour': '10', + 'minute': '0-59', + 'second': '0-59' # 10:00到10:59的每秒 + }, + { + 'hour': '11', + 'minute': '0-29', + 'second': '0-59' # 11:00到11:29的每秒(包含59秒) + }, + # 下午时间段: 13:00:00 到 14:59:59 + { + 'hour': '13-14', + 'minute': '0-59', + 'second': '0-59' # 13:00到14:59的每秒 + } + ] + + for idx, cron_params in enumerate(run_time_ranges): + self.scheduler.add_job(self.callback_run_no_quotes, 'cron', **cron_params, id=f"run_{idx}") + + if self.before_trade_day is not None: # 03:00 ~ 06:59 + random_hour = random.randint(0, 3) + 3 + random_minute = random.randint(0, 59) + self.scheduler.add_job(self.before_trade_day_wrapper, 'cron', hour=random_hour, minute=random_minute) + + if self.finish_trade_day is not None: # 16:05 ~ 16:15 + random_minute = random.randint(0, 10) + 5 + self.scheduler.add_job(self.finish_trade_day_wrapper, 'cron', hour=16, minute=random_minute) + + self.scheduler.add_job(self.prev_check_open_day, 'cron', hour=1, minute=0, second=0) + self.scheduler.add_job(self.check_before_finished, 'cron', hour=8, minute=55) # 检查当天是否完成准备 + self.scheduler.add_job(self.callback_open_no_quotes, 'cron', hour=9, minute=14, second=30) + self.scheduler.add_job(self.callback_close_no_quotes, 'cron', hour=11, minute=30, second=30) + self.scheduler.add_job(self.callback_open_no_quotes, 'cron', hour=12, minute=59, second=30) + self.scheduler.add_job(self.callback_close_no_quotes, 'cron', hour=15, minute=0, second=30) + self.scheduler.add_job(self.daily_summary, 'cron', hour=15, minute=1, second=0) + + try: + print('[定时器已启动]') + self.scheduler.start() + except KeyboardInterrupt: + print('[手动结束进程]') + except Exception as e: + print('策略定时器出错:', e) + finally: + self.delegate.shutdown() + + def create_scheduler(self): + if self.use_ap_scheduler: + from apscheduler.schedulers.blocking import BlockingScheduler + from apscheduler.executors.pool import ThreadPoolExecutor + executors = { + 'default': ThreadPoolExecutor(32), + } + job_defaults = { + 'coalesce': True, + 'misfire_grace_time': 180, + 'max_instances': 3 + } + self.scheduler = BlockingScheduler(timezone='Asia/Shanghai', executors=executors, job_defaults=job_defaults) + + def start_scheduler(self): + if self.use_ap_scheduler: + temp_now = datetime.datetime.now() + temp_date = temp_now.strftime('%Y-%m-%d') + temp_time = temp_now.strftime('%H:%M') + # 盘中执行需要补齐 + if '08:05' < temp_time < '15:30' and check_is_open_day(temp_date): + self.before_trade_day_wrapper() + self.near_trade_begin_wrapper() + if '09:15' < temp_time < '11:30' or '13:00' <= temp_time < '14:57': + self.callback_open_no_quotes() # 重启时如果在交易时间则订阅Tick + + self._start_scheduler() + + # ----------------------- + # 检查是否交易日 + # ----------------------- + def prev_check_open_day(self): + now = datetime.datetime.now() + curr_date = now.strftime('%Y-%m-%d') + curr_time = now.strftime('%H:%M') + print(f'[{curr_time}]', end='') + is_open_day = check_is_open_day(curr_date) + self.delegate.is_open_day = is_open_day + + +class HistorySubscriber(BaseSubscriber): + # ----------------------- + # 盘前下载数据缓存 + # ----------------------- + def _download_from_remote( + self, + target_codes: list, + start: str, + end: str, + adjust: ExitRight, + columns: list[str], + data_source: DataSource, + ): + print(f'Prepared TIME RANGE: {start} - {end}') + t0 = datetime.datetime.now() + print(f'Downloading {len(target_codes)} stocks:') + + group_size = 200 + down_count = 0 + for i in range(0, len(target_codes), group_size): + sub_codes = [sub_code for sub_code in target_codes[i:i + group_size]] + print(i, sub_codes) # 已更新数量 + + # TUSHARE 批量下载限制总共8000天条数据,所以暂时弃用 + # if data_source == DataSource.TUSHARE: + # # 使用 TUSHARE 数据源批量下载 + # dfs = get_ts_daily_histories(sub_codes, start, end, columns) + # self.cache_history.update(dfs) + # time.sleep(0.1) + + # 默认使用 AKSHARE 数据源 + for code in sub_codes: + df = get_daily_history(code, start, end, columns=columns, adjust=adjust, data_source=data_source) + time.sleep(0.5) + if df is not None: + self.cache_history[code] = df + down_count += 1 + + print(f'Download completed with {down_count} stock histories succeed!') + t1 = datetime.datetime.now() + print(f'Prepared TIME COST: {t1 - t0}') + + def _download_from_tdx(self, target_codes: list, start: str, end: str, adjust: str, columns: list[str]): + print(f'Prepared time range: {start} - {end}') + t0 = datetime.datetime.now() + + full_history = get_tdxzip_history(adjust=adjust) + self.history_day_klines = full_history + + days = len(get_trading_date_list(start, end)) + + i = 0 + for code in target_codes: + if code in full_history: + i += 1 + self.cache_history[code] = full_history[code][columns].tail(days).copy() + print(f'[HISTORY] Find {i}/{len(target_codes)} codes returned.') + + t1 = datetime.datetime.now() + print(f'Prepared TIME COST: {t1 - t0}') + + def download_cache_history( + self, + cache_path: str, # DATA SOURCE 是tushare的时候不需要 + code_list: list[str], + start: str, + end: str, + adjust: ExitRight, + columns: list[str], + data_source: DataSource, + ): + # ======== 每日一次性全量数据源 ======== + if data_source == DataSource.AKSHARE or data_source == DataSource.TDXZIP: + temp_indicators = load_pickle(cache_path) + if temp_indicators is not None and len(temp_indicators) > 0: + # 如果有缓存就读缓存 + self.cache_history.clear() + self.cache_history = {} + self.cache_history.update(temp_indicators) + print(f'{len(self.cache_history)} histories loaded from {cache_path}') + if self.messager is not None: + self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:' + f'历史{len(self.cache_history)}支') + else: + # 如果没缓存就刷新白名单 + self.cache_history.clear() + self.cache_history = {} + if data_source == DataSource.AKSHARE: + self._download_from_remote(code_list, start, end, adjust, columns, data_source) + else: + print('[提示] 使用TDX ZIP文件作为数据源,请在RUN代码中添加调度任务 check_xdxr_cache ' + '更新除权除息数据,建议运行时段在05:30之后') + print('[提示] 使用TDX ZIP文件作为数据源,请在RUN代码中建议在 near_trade_begin 中执行 ' + 'download_cache_history 获取历史数据,避免 before_trade_day 执行时间太早未更新除权信息') + self._download_from_tdx(code_list, start, end, adjust, columns) + + save_pickle(cache_path, self.cache_history) + print(f'{len(self.cache_history)} of {len(code_list)} histories saved to {cache_path}') + if self.messager is not None: + self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:' + f'历史{len(self.cache_history)}支') + + if data_source == DataSource.TDXZIP and self.history_day_klines is None: + self.history_day_klines = get_tdxzip_history(adjust=adjust) + + # ======== 预加载每日增量数据源 ======== + elif data_source == DataSource.TUSHARE or data_source == DataSource.MOOTDX: + hc = DailyHistoryCache() + hc.set_data_source(data_source=data_source) + if hc.daily_history is not None: + hc.daily_history.remove_recent_exit_right_histories(5) # 一周数据 + hc.daily_history.download_recent_daily(20) # 一个月数据 + # 下载后加载进内存 + start_date = datetime.datetime.strptime(start, '%Y%m%d') + end_date = datetime.datetime.strptime(end, '%Y%m%d') + delta = abs(end_date - start_date) + self.cache_history = hc.daily_history.get_subset_copy(code_list, delta.days + 1) + else: + if self.messager is not None: + self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:\n无法识别的数据源') + + + # 重新加载历史数据进内存 + def refresh_memory_history(self, code_list: list[str], start: str, end: str, data_source: DataSource): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + hc = DailyHistoryCache() + hc.set_data_source(data_source=data_source) + if hc.daily_history is not None: + start_date = datetime.datetime.strptime(start, '%Y%m%d') + end_date = datetime.datetime.strptime(end, '%Y%m%d') + delta = abs(end_date - start_date) + self.cache_history = hc.daily_history.get_subset_copy(code_list, delta.days + 1) diff --git a/delegate/daily_reporter.py b/delegate/daily_reporter.py index 81630b9..374fb80 100644 --- a/delegate/daily_reporter.py +++ b/delegate/daily_reporter.py @@ -4,7 +4,7 @@ import pandas as pd from xtquant import xtdata -from delegate.xt_delegate import BaseDelegate +from delegate.base_delegate import BaseDelegate from tools.constants import MSG_INNER_SEPARATOR, MSG_OUTER_SEPARATOR from tools.utils_basic import code_to_symbol @@ -50,15 +50,14 @@ def __init__( today_report_show_bank: bool = False, # 是否显示银行流水(国金QMT会卡死所以默认关闭) ): self.account_id = account_id - self.strategy_name = strategy_name self.delegate = delegate - + self.strategy_name = strategy_name self.path_deal = path_deal self.path_assets = path_assets self.messager = messager - self.today_report_show_bank = today_report_show_bank self.use_outside_data = use_outside_data + self.today_report_show_bank = today_report_show_bank self.stock_names = StockNames() def today_deal_report(self, today: str): diff --git a/delegate/xt_subscriber.py b/delegate/xt_subscriber.py index ff7a4df..f1594af 100644 --- a/delegate/xt_subscriber.py +++ b/delegate/xt_subscriber.py @@ -4,122 +4,91 @@ import pickle import random import threading -import traceback from typing import Dict, Callable, Optional import pandas as pd from xtquant import xtdata +from delegate.base_subscriber import HistorySubscriber from delegate.xt_delegate import XtDelegate -from delegate.daily_history import DailyHistoryCache from delegate.daily_reporter import DailyReporter -from tools.utils_cache import StockNames, InfoItem, check_is_open_day, get_trading_date_list -from tools.utils_cache import load_pickle, save_pickle, load_json, save_json +from tools.utils_cache import check_is_open_day from tools.utils_ding import BaseMessager -from tools.utils_mootdx import get_tdxzip_history -from tools.utils_remote import DataSource, ExitRight, get_daily_history, qmt_quote_to_tick +from tools.utils_remote import qmt_quote_to_tick -class BaseSubscriber: - pass - - -class XtSubscriber(BaseSubscriber): +class XtSubscriber(HistorySubscriber): def __init__( self, + # 基本信息 account_id: str, - strategy_name: str, delegate: Optional[XtDelegate], + strategy_name: str, path_deal: str, path_assets: str, + # 回调 execute_strategy: Callable, # 策略回调函数 execute_interval: int = 1, # 策略执行间隔,单位(秒) before_trade_day: Callable = None, # 盘前函数 near_trade_begin: Callable = None, # 盘后函数 finish_trade_day: Callable = None, # 盘后函数 - use_outside_data: bool = False, # 默认使用原版 QMT data (定期 call 数据但不传入quotes) + # 订阅 use_ap_scheduler: bool = False, # 默认使用旧版 schedule (尽可能向前兼容旧策略吧) + # 通知 ding_messager: BaseMessager = None, - open_tick_memory_cache: bool = False, - tick_memory_data_frame: bool = False, + # 日报 open_today_deal_report: bool = False, # 每日交易记录报告 open_today_hold_report: bool = False, # 每日持仓记录报告 today_report_show_bank: bool = False, # 是否显示银行流水(国金QMT会卡死所以默认关闭) + # tick 缓存 + open_tick_memory_cache: bool = False, + tick_memory_data_frame: bool = False, ): - self.account_id = '**' + str(account_id)[-4:] - self.strategy_name = strategy_name - self.delegate = delegate - if self.delegate is not None: - self.delegate.subscriber = self - - self.path_deal = path_deal - self.path_assets = path_assets - - self.execute_strategy = execute_strategy - self.execute_interval = execute_interval - self.before_trade_day = before_trade_day # 提前准备某些耗时长的任务 - self.near_trade_begin = near_trade_begin # 有些数据临近开盘才更新,这里保证内存里的数据正确 - self.finish_trade_day = finish_trade_day # 盘后及时做一些总结汇报入库类的整理工作 - self.messager = ding_messager - - self.lock_quotes_update = threading.Lock() # 聚合实时打点缓存的锁 - - self.cache_quotes: Dict[str, Dict] = {} # 记录实时的价格信息 - self.cache_limits: Dict[str, str] = { # 限制执行次数的缓存集合 - 'prev_seconds': '', # 限制每秒一次跑策略扫描的缓存 - 'prev_minutes': '', # 限制每分钟屏幕心跳换行的缓存 - } - self.cache_history: Dict[str, pd.DataFrame] = {} # 记录历史日线行情的信息 { code: DataFrame } + super().__init__( + account_id=account_id, + delegate=delegate, + strategy_name=strategy_name, + path_deal=path_deal, + path_assets=path_assets, + execute_strategy=execute_strategy, + execute_interval=execute_interval, + before_trade_day=before_trade_day, + near_trade_begin=near_trade_begin, + finish_trade_day=finish_trade_day, + open_today_deal_report=open_today_deal_report, + open_today_hold_report=open_today_hold_report, + today_report_show_bank=today_report_show_bank, + ding_messager=ding_messager, + ) + self.use_ap_scheduler = use_ap_scheduler + self.create_scheduler() self.open_tick = open_tick_memory_cache self.is_ticks_df = tick_memory_data_frame self.quick_ticks: bool = False # 是否开启quick tick模式 self.today_ticks: Dict[str, list | pd.DataFrame] = {} # 记录tick的历史信息 + self.lock_quotes_update = threading.Lock() # 聚合实时打点缓存的锁 - self.open_today_deal_report = open_today_deal_report - self.open_today_hold_report = open_today_hold_report - self.today_report_show_bank = today_report_show_bank + self.cache_quotes: Dict[str, Dict] = {} # 记录实时的价格信息 self.code_list = ['000001.SH'] # 默认只有上证指数 - self.stock_names = StockNames() self.last_callback_time = datetime.datetime.now() # 上次返回quotes 时间 - # 这个成员变量区别于cache_history,保存全部股票的日线数据550天,cache_history只包含code_list中指定天数数据 - self.history_day_klines : Dict[str, pd.DataFrame] = {} - self.__extend_codes = ['399001.SZ', '510230.SH', '512680.SH', '159915.SZ', '510500.SH', '588000.SH', '159101.SZ', '399006.SZ', '159315.SZ'] - self.use_outside_data = use_outside_data - self.use_ap_scheduler = use_ap_scheduler - if self.use_outside_data: - self.use_ap_scheduler = True # 如果use_outside_data 被设置为True,则需强制使用apscheduler - self.daily_reporter = DailyReporter( - self.account_id, - self.strategy_name, - self.delegate, - self.path_deal, - self.path_assets, - self.messager, - self.use_outside_data, - self.today_report_show_bank, + account_id=self.account_id, + delegate=self.delegate, + strategy_name=self.strategy_name, + path_deal=self.path_deal, + path_assets=self.path_assets, + messager=self.messager, + use_outside_data=False, + today_report_show_bank=self.today_report_show_bank, ) - if self.use_ap_scheduler: - from apscheduler.schedulers.blocking import BlockingScheduler - from apscheduler.executors.pool import ThreadPoolExecutor - executors = { - 'default': ThreadPoolExecutor(32), - } - job_defaults = { - 'coalesce': True, - 'misfire_grace_time': 180, - 'max_instances': 3 - } - self.scheduler = BlockingScheduler(timezone='Asia/Shanghai', executors=executors, job_defaults=job_defaults) - if self.is_ticks_df: self.tick_df_cols = ['time', 'price', 'high', 'low', 'volume', 'amount'] \ + [f'askPrice{i}' for i in range(1, 6)] \ @@ -172,46 +141,6 @@ def callback_sub_whole(self, quotes: Dict) -> None: print(print_mark, end='') # 每秒钟开始的时候输出一个点 - def callback_run_no_quotes(self): - if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): - return - - now = datetime.datetime.now() - self.last_callback_time = now - - curr_date = now.strftime('%Y-%m-%d') - curr_time = now.strftime('%H:%M') - - # 每分钟输出一行开头 - if self.cache_limits['prev_minutes'] != curr_time: - self.cache_limits['prev_minutes'] = curr_time - print(f'\n[{curr_time}]', end='') - - curr_seconds = now.strftime('%S') - if self.cache_limits['prev_seconds'] != curr_seconds: - self.cache_limits['prev_seconds'] = curr_seconds - - if int(curr_seconds) % self.execute_interval == 0: - print('.', end='') # cache_quotes 肯定没数据,这里就是输出观察线程健康 - # str(%Y-%m-%d), str(%H:%M), str(%S) - self.execute_strategy(curr_date, curr_time, curr_seconds, {}) - - def callback_open_no_quotes(self): - if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): - return - - if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:开启') - print('[启动策略]', end='') - - def callback_close_no_quotes(self): - if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): - return - - print('\n[关闭策略]') - if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:结束') - # ----------------------- # 监测主策略执行 # ----------------------- @@ -233,7 +162,7 @@ def callback_monitor(self): self.resubscribe_tick(notice=True) # ----------------------- - # 订阅tick相关 + # 订阅 tick 相关 # ----------------------- def subscribe_tick(self, resume: bool = False): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): @@ -279,10 +208,10 @@ def update_code_list(self, code_list: list[str]): self.code_list.extend(self.__extend_codes[:extend]) # 防止数据太少长时间不返回数据导致断流 # ----------------------- - # 盘中实时的tick历史 + # 盘中实时的 tick 历史 # ----------------------- def record_tick_to_memory(self, quotes): - # 记录 tick 历史 + # 记录 tick 历史到内存 if self.is_ticks_df: for code in quotes: quote = quotes[code] @@ -318,7 +247,7 @@ def clean_ticks_history(self): self.today_ticks.clear() self.today_ticks = {} - print(f"已清除tick缓存") + print(f"[提示] 已清除tick缓存") def save_tick_history(self): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): @@ -328,185 +257,27 @@ def save_tick_history(self): pickle_file = f'./_cache/debug/tick_history_{self.strategy_name}.pkl' with open(pickle_file, 'wb') as f: pickle.dump(self.today_ticks, f) - print(f"当日tick数据已存储为 {pickle_file} 文件") + print(f"[提示] 当日tick数据已存储为 {pickle_file} 文件") else: json_file = f'./_cache/debug/tick_history_{self.strategy_name}.json' with open(json_file, 'w') as file: json.dump(self.today_ticks, file, indent=4) - print(f"当日tick数据已存储为 {json_file} 文件") + print(f"[提示] 当日tick数据已存储为 {json_file} 文件") - # ----------------------- - # 盘前下载数据缓存 - # ----------------------- - def _download_from_remote( - self, - target_codes: list, - start: str, - end: str, - adjust: ExitRight, - columns: list[str], - data_source: DataSource, - ): - print(f'Prepared TIME RANGE: {start} - {end}') - t0 = datetime.datetime.now() - print(f'Downloading {len(target_codes)} stocks:') - - group_size = 200 - down_count = 0 - for i in range(0, len(target_codes), group_size): - sub_codes = [sub_code for sub_code in target_codes[i:i + group_size]] - print(i, sub_codes) # 已更新数量 - - # TUSHARE 批量下载限制总共8000天条数据,所以暂时弃用 - # if data_source == DataSource.TUSHARE: - # # 使用 TUSHARE 数据源批量下载 - # dfs = get_ts_daily_histories(sub_codes, start, end, columns) - # self.cache_history.update(dfs) - # time.sleep(0.1) - - # 默认使用 AKSHARE 数据源 - for code in sub_codes: - df = get_daily_history(code, start, end, columns=columns, adjust=adjust, data_source=data_source) - time.sleep(0.5) - if df is not None: - self.cache_history[code] = df - down_count += 1 - - print(f'Download completed with {down_count} stock histories succeed!') - t1 = datetime.datetime.now() - print(f'Prepared TIME COST: {t1 - t0}') - - def _download_from_tdx(self, target_codes: list, start: str, end: str, adjust: str, columns: list[str]): - print(f'Prepared time range: {start} - {end}') - t0 = datetime.datetime.now() - - full_history = get_tdxzip_history(adjust=adjust) - self.history_day_klines = full_history - - days = len(get_trading_date_list(start, end)) - - i = 0 - for code in target_codes: - if code in full_history: - i += 1 - self.cache_history[code] = full_history[code][columns].tail(days).copy() - print(f'[HISTORY] Find {i}/{len(target_codes)} codes returned.') - - t1 = datetime.datetime.now() - print(f'Prepared TIME COST: {t1 - t0}') - - def download_cache_history( - self, - cache_path: str, # DATA SOURCE 是tushare的时候不需要 - code_list: list[str], - start: str, - end: str, - adjust: ExitRight, - columns: list[str], - data_source: DataSource, - ): - # ======== 每日一次性全量数据源 ======== - if data_source == DataSource.AKSHARE or data_source == DataSource.TDXZIP: - temp_indicators = load_pickle(cache_path) - if temp_indicators is not None and len(temp_indicators) > 0: - # 如果有缓存就读缓存 - self.cache_history.clear() - self.cache_history = {} - self.cache_history.update(temp_indicators) - print(f'{len(self.cache_history)} histories loaded from {cache_path}') - if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:' - f'历史{len(self.cache_history)}支') - else: - # 如果没缓存就刷新白名单 - self.cache_history.clear() - self.cache_history = {} - if data_source == DataSource.AKSHARE: - self._download_from_remote(code_list, start, end, adjust, columns, data_source) - else: - print('[提醒] 使用TDX ZIP文件作为数据源,请在RUN代码中添加调度任务check_xdxr_cache更新除权除息数据,建议运行时段在05:30之后。') - print('[提醒] 使用TDX ZIP文件作为数据源,请在RUN代码中建议在near_trade_begin中执行download_cache_history获取历史数据,避免before_trade_day执行时间太早未更新除权信息。') - self._download_from_tdx(code_list, start, end, adjust, columns) - - save_pickle(cache_path, self.cache_history) - print(f'{len(self.cache_history)} of {len(code_list)} histories saved to {cache_path}') - if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:' - f'历史{len(self.cache_history)}支') - - if data_source == DataSource.TDXZIP and self.history_day_klines is None: - self.history_day_klines = get_tdxzip_history(adjust=adjust) - - # ======== 预加载每日增量数据源 ======== - elif data_source == DataSource.TUSHARE or data_source == DataSource.MOOTDX: - hc = DailyHistoryCache() - hc.set_data_source(data_source=data_source) - if hc.daily_history is not None: - hc.daily_history.remove_recent_exit_right_histories(5) # 一周数据 - hc.daily_history.download_recent_daily(20) # 一个月数据 - # 下载后加载进内存 - start_date = datetime.datetime.strptime(start, '%Y%m%d') - end_date = datetime.datetime.strptime(end, '%Y%m%d') - delta = abs(end_date - start_date) - self.cache_history = hc.daily_history.get_subset_copy(code_list, delta.days + 1) - else: - if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:\n无法识别的数据源') - - def refresh_memory_history( - self, - code_list: list[str], - start: str, - end: str, - data_source: DataSource, - ): - if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): - return - - hc = DailyHistoryCache() - hc.set_data_source(data_source=data_source) - if hc.daily_history is not None: - # 重新加载进内存 - start_date = datetime.datetime.strptime(start, '%Y%m%d') - end_date = datetime.datetime.strptime(end, '%Y%m%d') - delta = abs(end_date - start_date) - self.cache_history = hc.daily_history.get_subset_copy(code_list, delta.days + 1) - - # ----------------------- - # 盘后报告总结 - # ----------------------- - def daily_summary(self): + # 检查是否完成盘前准备 + def check_before_finished(self): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): return - curr_date = datetime.datetime.now().strftime('%Y-%m-%d') - - if self.open_today_deal_report: - try: - self.daily_reporter.today_deal_report(today=curr_date) - except Exception as e: - print('Report deal failed: ', e) - traceback.print_exc() - - if self.open_today_hold_report: - try: - if self.delegate is not None: - positions = self.delegate.check_positions() - self.daily_reporter.today_hold_report(today=curr_date, positions=positions) - else: - print('Missing delegate to complete reporting!') - except Exception as e: - print('Report position failed: ', e) - traceback.print_exc() - - try: - if self.delegate is not None: - asset = self.delegate.check_asset() - self.daily_reporter.check_asset(today=curr_date, asset=asset) - except Exception as e: - print('Report asset failed: ', e) - traceback.print_exc() - + if (self.before_trade_day is not None or self.near_trade_begin is not None) \ + and ( + self.curr_trade_date != datetime.datetime.now().strftime("%Y-%m-%d") + or len(self.cache_history) < 1 + ): + print('[警告] 盘前准备未完成,尝试重新执行盘前函数') + self.before_trade_day_wrapper() + self.near_trade_begin_wrapper() + print(f'[提示] 当前交易日:{self.curr_trade_date}') # ----------------------- # 定时器 @@ -521,102 +292,12 @@ def before_trade_day_wrapper(self): self.history_day_klines.clear() self.code_list = ['000001.SH'] # 默认只有上证指数 - if self.before_trade_day is not None: self.before_trade_day() self.curr_trade_date = datetime.datetime.now().strftime('%Y-%m-%d') - def near_trade_begin_wrapper(self): - if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): - return - - if self.near_trade_begin is not None: - self.near_trade_begin() - if self.before_trade_day is None: # 没有设置before_trade_day 情况 - self.curr_trade_date = datetime.datetime.now().strftime('%Y-%m-%d') - print(f'今日盘前准备工作已完成') - def finish_trade_day_wrapper(self): - if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): - return - - if self.finish_trade_day is not None: - self.finish_trade_day() - - # 检查是否完成盘前准备 - # @check_open_day - def check_before_finished(self): - if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): - return - - if ( - self.before_trade_day is not None or self.near_trade_begin is not None - ) and ( - self.curr_trade_date != datetime.datetime.now().strftime("%Y-%m-%d") - or len(self.cache_history) < 1 - ): - print('[ERROR]盘前准备未完成,尝试重新执行盘前函数') - self.before_trade_day_wrapper() - self.near_trade_begin_wrapper() - print(f'当前交易日:[{self.curr_trade_date}]。') - - def start_scheduler_without_qmt_data(self): - run_time_ranges = [ - # 上午时间段: 09:15:00 到 11:29:59 - { - 'hour': '9', - 'minute': '15-59', - 'second': '0-59' # 9:15到9:59的每秒 - }, - { - 'hour': '10', - 'minute': '0-59', - 'second': '0-59' # 10:00到10:59的每秒 - }, - { - 'hour': '11', - 'minute': '0-29', - 'second': '0-59' # 11:00到11:29的每秒(包含59秒) - }, - # 下午时间段: 13:00:00 到 14:59:59 - { - 'hour': '13-14', - 'minute': '0-59', - 'second': '0-59' # 13:00到14:59的每秒 - } - ] - - for idx, cron_params in enumerate(run_time_ranges): - self.scheduler.add_job(self.callback_run_no_quotes, 'cron', **cron_params, id=f"run_{idx}") - - if self.before_trade_day is not None: # 03:00 ~ 06:59 - random_hour = random.randint(0, 3) + 3 - random_minute = random.randint(0, 59) - self.scheduler.add_job(self.before_trade_day_wrapper, 'cron', hour=random_hour, minute=random_minute) - - if self.finish_trade_day is not None: # 16:05 ~ 16:15 - random_minute = random.randint(0, 10) + 5 - self.scheduler.add_job(self.finish_trade_day_wrapper, 'cron', hour=16, minute=random_minute) - - self.scheduler.add_job(self.prev_check_open_day, 'cron', hour=1, minute=0, second=0) - self.scheduler.add_job(self.check_before_finished, 'cron', hour=8, minute=55) # 检查当天是否完成准备 - self.scheduler.add_job(self.callback_open_no_quotes, 'cron', hour=9, minute=14, second=59) - self.scheduler.add_job(self.callback_close_no_quotes, 'cron', hour=11, minute=30, second=0) - self.scheduler.add_job(self.callback_open_no_quotes, 'cron', hour=12, minute=59, second=59) - self.scheduler.add_job(self.callback_close_no_quotes, 'cron', hour=15, minute=0, second=0) - self.scheduler.add_job(self.daily_summary, 'cron', hour=15, minute=1, second=0) - - try: - print('[定时器已启动]') - self.scheduler.start() - except KeyboardInterrupt: - print('[手动结束进程]') - except Exception as e: - print('策略定时器出错:', e) - finally: - self.delegate.shutdown() - - def start_scheduler_with_qmt_data(self): + def _start_qmt_scheduler(self): # 默认定时任务列表 cron_jobs = [ ['01:00', self.prev_check_open_day, None], @@ -725,51 +406,7 @@ def start_scheduler(self): self.near_trade_begin_wrapper() if '09:15' < temp_time < '11:30' or '13:00' <= temp_time < '14:57': self.subscribe_tick() # 重启时如果在交易时间则订阅Tick - - if self.use_outside_data: - self.start_scheduler_without_qmt_data() - return - else: - self.start_scheduler_with_qmt_data() - - # ----------------------- - # 检查是否交易日 - # ----------------------- - def prev_check_open_day(self): - now = datetime.datetime.now() - curr_date = now.strftime('%Y-%m-%d') - curr_time = now.strftime('%H:%M') - print(f'[{curr_time}]', end='') - is_open_day = check_is_open_day(curr_date) - self.delegate.is_open_day = is_open_day - - -# ----------------------- -# 持仓自动发现 -# ----------------------- -def update_position_held(lock: threading.Lock, delegate: XtDelegate, path: str): - with lock: - positions = delegate.check_positions() - held_info = load_json(path) - - # 添加未被缓存记录的持仓:默认当日买入 - for position in positions: - if position.can_use_volume > 0: - if position.stock_code not in held_info.keys(): - held_info[position.stock_code] = {InfoItem.DayCount: 0} - - # 删除已清仓的held_info记录 - if positions is not None and len(positions) > 0: - position_codes = [position.stock_code for position in positions] - print('当前持仓:', position_codes) - holding_codes = list(held_info.keys()) - for code in holding_codes: - if len(code) > 0 and code[0] != '_' and (code not in position_codes): - del held_info[code] - else: - print('当前空仓!') - - save_json(path, held_info) + self._start_qmt_scheduler() # ----------------------- diff --git a/run_ai_gen.py b/run_ai_gen.py index ffd4a0a..d8682e3 100644 --- a/run_ai_gen.py +++ b/run_ai_gen.py @@ -7,7 +7,8 @@ from tools.utils_ding import DingMessager from tools.utils_remote import DataSource, ExitRight, concat_ak_quote_dict -from delegate.xt_subscriber import XtSubscriber, update_position_held +from delegate.base_delegate import update_position_held +from delegate.xt_subscriber import XtSubscriber from trader.pools import StocksPoolWhitePrefixesMA as Pool from trader.buyer import BaseBuyer as Buyer diff --git a/run_redis_pull.py b/run_redis_pull.py index e33ab42..d44b22c 100644 --- a/run_redis_pull.py +++ b/run_redis_pull.py @@ -7,7 +7,8 @@ from tools.utils_ding import DingMessager from tools.utils_remote import get_wencai_codes, get_mootdx_quotes -from delegate.xt_subscriber import XtSubscriber, update_position_held +from delegate.base_delegate import update_position_held +from delegate.base_subscriber import HistorySubscriber from trader.pools import StocksPoolBlackWencai as Pool from trader.buyer import BaseBuyer as Buyer @@ -267,7 +268,7 @@ def redis_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu delegate=my_delegate, parameters=SellConf, ) - my_suber = XtSubscriber( + my_suber = HistorySubscriber( account_id=QMT_ACCOUNT_ID, strategy_name=STRATEGY_NAME, delegate=my_delegate, @@ -275,8 +276,6 @@ def redis_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu path_assets=PATH_ASSETS, execute_strategy=empty_execute_strategy, before_trade_day=before_trade_day, - use_outside_data=True, - use_ap_scheduler=True, ding_messager=DING_MESSAGER, open_today_deal_report=True, open_today_hold_report=True, diff --git a/run_remote.py b/run_remote.py index a38331a..148c77a 100644 --- a/run_remote.py +++ b/run_remote.py @@ -7,7 +7,8 @@ from tools.utils_ding import DingMessager from tools.utils_remote import DataSource, ExitRight -from delegate.xt_subscriber import XtSubscriber, update_position_held, xt_get_ticks +from delegate.base_delegate import update_position_held +from delegate.xt_subscriber import XtSubscriber, xt_get_ticks from trader.pools import StocksPoolBlackEmpty as Pool from trader.buyer import BaseBuyer as Buyer diff --git a/run_shield.py b/run_shield.py index 78d4280..9fcad98 100644 --- a/run_shield.py +++ b/run_shield.py @@ -1,15 +1,19 @@ import logging +import threading +from typing import Dict, Set, List -from credentials import * - +from credentials import ( + DING_SECRET, DING_TOKENS, CACHE_PROD_PATH, CACHE_TEST_PATH, + QMT_ACCOUNT_ID, QMT_CLIENT_PATH +) from tools.utils_basic import logging_init, is_symbol -from tools.utils_cache import * +from tools.utils_cache import all_held_inc, update_max_prices from tools.utils_ding import DingMessager -from delegate.xt_subscriber import XtSubscriber, update_position_held +from delegate.base_delegate import update_position_held +from delegate.xt_subscriber import XtSubscriber from trader.pools import StocksPoolWhiteIndexes as Pool -from trader.buyer import BaseBuyer as Buyer from trader.seller_groups import ShieldGroupSeller as Seller diff --git a/run_swords.py b/run_swords.py index 848739e..c627fd4 100644 --- a/run_swords.py +++ b/run_swords.py @@ -1,12 +1,18 @@ import logging - -from credentials import * - +import threading +import datetime +from typing import Dict, Set, List + +from credentials import ( + DING_SECRET, DING_TOKENS, CACHE_PROD_PATH, CACHE_TEST_PATH, + QMT_ACCOUNT_ID, QMT_CLIENT_PATH +) from tools.utils_basic import logging_init, is_symbol, time_diff_seconds -from tools.utils_cache import * +from tools.utils_cache import all_held_inc from tools.utils_ding import DingMessager -from delegate.xt_subscriber import XtSubscriber, update_position_held +from delegate.base_delegate import update_position_held +from delegate.xt_subscriber import XtSubscriber from trader.pools import StocksPoolWhiteCustomSymbol as Pool from trader.buyer import BaseBuyer as Buyer diff --git a/run_wencai_qmt.py b/run_wencai_qmt.py index f7aa1d3..6bb2f5b 100644 --- a/run_wencai_qmt.py +++ b/run_wencai_qmt.py @@ -1,12 +1,18 @@ import logging +import threading +from typing import Dict, Set, List -from credentials import * +from credentials import ( + DING_SECRET, DING_TOKENS, CACHE_PROD_PATH, CACHE_TEST_PATH, + QMT_ACCOUNT_ID, QMT_CLIENT_PATH +) from tools.utils_basic import logging_init, is_symbol, debug -from tools.utils_cache import * +from tools.utils_cache import all_held_inc, update_max_prices from tools.utils_ding import DingMessager from tools.utils_remote import get_wencai_codes -from delegate.xt_subscriber import XtSubscriber, update_position_held, xt_get_ticks +from delegate.base_delegate import update_position_held +from delegate.xt_subscriber import XtSubscriber, xt_get_ticks from trader.pools import StocksPoolBlackWencai as Pool from trader.buyer import BaseBuyer as Buyer @@ -150,7 +156,7 @@ def check_stock_codes(selected_codes: list[str], quotes: dict) -> dict[str, dict def scan_buy(quotes: Dict, curr_date: str, positions: List) -> None: selected_codes = pull_stock_codes() - print(f'[{len(selected_codes)}]', end='') + debug(f'[{len(selected_codes)}]', end='') selections = {} if selected_codes is not None and len(selected_codes) > 0: diff --git a/run_wencai_tdx.py b/run_wencai_tdx.py index d238315..c8a11e9 100644 --- a/run_wencai_tdx.py +++ b/run_wencai_tdx.py @@ -12,7 +12,8 @@ from tools.utils_ding import DingMessager from tools.utils_remote import get_wencai_codes, get_mootdx_quotes -from delegate.xt_subscriber import XtSubscriber, update_position_held +from delegate.base_delegate import update_position_held +from delegate.base_subscriber import HistorySubscriber from trader.pools import StocksPoolBlackWencai as Pool from trader.buyer import BaseBuyer as Buyer @@ -252,7 +253,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo delegate=my_delegate, parameters=SellConf, ) - my_suber = XtSubscriber( + my_suber = HistorySubscriber( account_id=QMT_ACCOUNT_ID, strategy_name=STRATEGY_NAME, delegate=my_delegate, @@ -260,8 +261,6 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo path_assets=PATH_ASSETS, execute_strategy=execute_strategy, before_trade_day=before_trade_day, - use_outside_data=True, - use_ap_scheduler=True, ding_messager=DING_MESSAGER, open_today_deal_report=True, open_today_hold_report=True, diff --git a/trader/seller_components.py b/trader/seller_components.py index 6fcc8b2..f0e098c 100644 --- a/trader/seller_components.py +++ b/trader/seller_components.py @@ -1,9 +1,9 @@ import logging - -from mytt.MyTT_advance import * -# from mytt.MyTT_custom import * from typing import Dict, Optional +import pandas as pd + +from mytt.MyTT import MA, MACD, CCI, WR from xtquant.xttype import XtPosition from tools.utils_basic import get_limit_up_price from tools.utils_remote import concat_ak_quote_dict From 66637687d7ad650f3072fc9709b13947d0c22ba0 Mon Sep 17 00:00:00 2001 From: Junchao Date: Sun, 7 Dec 2025 15:31:40 +0800 Subject: [PATCH 06/16] little polish & fix --- delegate/base_subscriber.py | 8 ++--- run_swords_tdx.py | 11 ++---- tools/utils_dfcf.py | 71 +++++++++++++++++++++++++++++++++++-- tools/utils_remote.py | 4 +-- 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/delegate/base_subscriber.py b/delegate/base_subscriber.py index 800df2e..d8b2843 100644 --- a/delegate/base_subscriber.py +++ b/delegate/base_subscriber.py @@ -124,7 +124,7 @@ def callback_open_no_quotes(self): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): return - print('[启动策略]', end='') + print('[启动策略]') if self.messager is not None: self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:开启') @@ -136,13 +136,13 @@ def callback_close_no_quotes(self): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): return - if self.custom_end_unsub is not None: - threading.Thread(target=self.custom_end_unsub).start() + print('\n[关闭策略]') if self.messager is not None: self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:结束') - print('\n[关闭策略]') + if self.custom_end_unsub is not None: + threading.Thread(target=self.custom_end_unsub).start() def update_code_list(self, code_list: list[str]): self.code_list = code_list diff --git a/run_swords_tdx.py b/run_swords_tdx.py index fa0ab90..a9ce597 100644 --- a/run_swords_tdx.py +++ b/run_swords_tdx.py @@ -6,7 +6,8 @@ from tools.utils_cache import * from tools.utils_ding import DingMessager -from delegate.xt_subscriber import XtSubscriber, update_position_held +from delegate.base_delegate import update_position_held +from delegate.xt_subscriber import XtSubscriber from trader.pools import StocksPoolWhiteCustomSymbol as Pool from trader.buyer import BaseBuyer as Buyer @@ -173,13 +174,7 @@ def select_stocks( return selections -def scan_buy( - quotes: Dict, - curr_date: str, - curr_time: str, - curr_seconds: str, - positions: List, -) -> None: +def scan_buy(quotes: Dict, curr_date: str, curr_time: str, curr_seconds: str, positions: List) -> None: selections = select_stocks(quotes, curr_time, curr_seconds) # debug(f'本次扫描:{len(quotes)}, 选股{selections}) diff --git a/tools/utils_dfcf.py b/tools/utils_dfcf.py index d90f7d7..08cde0a 100644 --- a/tools/utils_dfcf.py +++ b/tools/utils_dfcf.py @@ -1,12 +1,15 @@ ''' -https://www.myquant.cn/docs2/sdk/python/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B.html +https://www.myquant.cn/docs2/sdk/python/API%E4%BB%8B%E7%BB%8D/%E4%BA%A4%E6%98%93%E5%87%BD%E6%95%B0.html 需要另建一个虚拟环境,pandas改为1.5.3版本 需要在新环境安装 pip install gm ''' import logging +from abc import ABC + import pandas as pd from gm.api import * +from delegate.base_delegate import BaseDelegate from tools.utils_basic import symbol_to_code, code_to_symbol @@ -138,9 +141,71 @@ def update_cache_quote(quotes: dict[str, dict], bars: list[dict], curr_time: str # =========== # 回测Delegate # =========== -class EmDelegate: + + +DEFAULT_STRATEGY_NAME = '回测策略' + + +class EmDelegate(BaseDelegate, ABC): """ 主要用东方财富内置的掘金系统做回测,因为可用的数据更多一些 为了跟GmDelegate作区分,起名EasyMoneyDelegate """ - pass + + def check_asset(self) -> any: + pass + + def check_orders(self) -> list: + pass + + def check_positions(self) -> list: + pass + + def order_market_open( + self, + code: str, + price: float, + volume: int, + remark: str, + strategy_name: str = DEFAULT_STRATEGY_NAME, + ) -> None: + pass + + def order_market_close( + self, + code: str, + price: float, + volume: int, + remark: str, + strategy_name: str = DEFAULT_STRATEGY_NAME, + ) -> None: + pass + + def order_limit_open( + self, + code: str, + price: float, + volume: int, + remark: str, + strategy_name: str = DEFAULT_STRATEGY_NAME, + ) -> None: + pass + + def order_limit_close( + self, + code: str, + price: float, + volume: int, + remark: str, + strategy_name: str = DEFAULT_STRATEGY_NAME, + ) -> None: + pass + + def order_cancel_all(self, strategy_name: str = DEFAULT_STRATEGY_NAME) -> None: + pass + + def order_cancel_buy(self, code: str, strategy_name: str = DEFAULT_STRATEGY_NAME) -> None: + pass + + def order_cancel_sell(self, code: str, strategy_name: str = DEFAULT_STRATEGY_NAME) -> None: + pass diff --git a/tools/utils_remote.py b/tools/utils_remote.py index 4ed85d5..c91b3d9 100644 --- a/tools/utils_remote.py +++ b/tools/utils_remote.py @@ -540,8 +540,8 @@ def get_bao_daily_history( df['high'] = df['high'].astype(float) df['low'] = df['low'].astype(float) df['close'] = df['close'].astype(float) - df['volume'] = df['volume'].astype(int) / 100 - df['amount'] = df['amount'].astype(float) + df['volume'] = df['volume'].replace('', '0').astype(int) / 100 + df['amount'] = df['amount'].replace('', '0').astype(float) if df is not None and len(df) > 0: if columns is not None: From ba9d0db49e3e7802fb600409289dd5d08d2953f5 Mon Sep 17 00:00:00 2001 From: dominicx Date: Wed, 10 Dec 2025 23:22:45 +0800 Subject: [PATCH 07/16] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=9B=98=E5=89=8D?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=98=AF=E5=90=A6=E5=AE=8C=E6=88=90=E6=A3=80?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 极端情况prev_check_open_day未执行或执行出现问题 --- delegate/xt_subscriber.py | 1 + 1 file changed, 1 insertion(+) diff --git a/delegate/xt_subscriber.py b/delegate/xt_subscriber.py index f1594af..d081d6c 100644 --- a/delegate/xt_subscriber.py +++ b/delegate/xt_subscriber.py @@ -275,6 +275,7 @@ def check_before_finished(self): or len(self.cache_history) < 1 ): print('[警告] 盘前准备未完成,尝试重新执行盘前函数') + self.prev_check_open_day() self.before_trade_day_wrapper() self.near_trade_begin_wrapper() print(f'[提示] 当前交易日:{self.curr_trade_date}') From 949f7610a8c21bd1d1e3b816397e26c37f6edad1 Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:02:42 +0800 Subject: [PATCH 08/16] little polish --- delegate/daily_reporter.py | 4 +-- run_redis_pull.py | 52 +++++++++++++++++++++----------------- tools/utils_remote.py | 2 +- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/delegate/daily_reporter.py b/delegate/daily_reporter.py index 22f9da2..a7dae7c 100644 --- a/delegate/daily_reporter.py +++ b/delegate/daily_reporter.py @@ -127,7 +127,7 @@ def today_hold_report(self, today: str, positions): total_change < -0.000001) ratio_prefix = '+' if ratio_change > 0 else '' ratio_change = colour_text( - f'{ratio_prefix}{ratio_change * 100:.2f}%', + f'{ratio_prefix}{ratio_change * 100:.1f}%', ratio_change > 0.000001, ratio_change < -0.000001) @@ -136,7 +136,7 @@ def today_hold_report(self, today: str, positions): f'{self.stock_names.get_name(code)} ' \ f'{curr_price * vol:.2f}元' text += MSG_INNER_SEPARATOR - text += f'成本 {open_price:.3f} 盈亏 {total_change} ({ratio_change})' + text += f'成本 {open_price:.3f} 浮 {total_change} ({ratio_change})' title = f'[{self.account_id}]{self.strategy_name} 持仓统计' text = f'{title}\n\n[{today}] 持仓{hold_count}支\n{text}' diff --git a/run_redis_pull.py b/run_redis_pull.py index d44b22c..70f93ec 100644 --- a/run_redis_pull.py +++ b/run_redis_pull.py @@ -1,5 +1,4 @@ import logging -import redis from credentials import * from tools.utils_basic import logging_init, is_symbol, debug @@ -16,6 +15,12 @@ from selector.select_wencai import get_prompt +import redis + +REDIS_HOST = '192.168.1.6' +REDIS_PORT = 6379 +REDIS_CHANNEL = 'quotes_data' +my_redis = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0).pubsub() STRATEGY_NAME = '数据接收' SELECT_PROMPT = get_prompt('') @@ -23,11 +28,6 @@ IS_PROD = False # 生产环境标志:False 表示使用掘金模拟盘 True 表示使用QMT账户下单交易 IS_DEBUG = True # 日志输出标记:控制台是否打印debug方法的输出 -REDIS_HOST = '192.168.1.6' -REDIS_PORT = 6379 -REDIS_CHANNEL = 'quotes_data' -my_redis = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0) - PATH_BASE = CACHE_PROD_PATH if IS_PROD else CACHE_TEST_PATH PATH_ASSETS = PATH_BASE + '/assets.csv' # 记录历史净值 PATH_DEAL = PATH_BASE + '/deal_hist.csv' # 记录历史成交 @@ -176,26 +176,31 @@ def empty_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu return curr_date is None and curr_time is None and curr_seconds is None and curr_quotes is None -def redis_subscriber(): - sub = my_redis.pubsub() - sub.subscribe(REDIS_CHANNEL) # 订阅频道 - +def redis_subscribe(): print('[开始监听数据]') - - my_code_set = set(my_pool.get_code_list()) - for message in sub.listen(): - if message['type'] == 'message': - data = json.loads(message['data']) - curr_date = data['curr_date'] - curr_time = data['curr_time'] - curr_seconds = data['curr_seconds'] - curr_quotes = data['curr_quotes'] - pool_quotes = {code: curr_quotes[code] for code in my_code_set if code in curr_quotes} - redis_execute_strategy(curr_date, curr_time, curr_seconds, pool_quotes) + my_redis.subscribe(REDIS_CHANNEL) # 订阅频道 + for message in my_redis.listen(): + try: + if message['type'] == 'message': + data = json.loads(message['data']) + curr_date = data['curr_date'] + curr_time = data['curr_time'] + curr_seconds = data['curr_seconds'] + curr_quotes = data['curr_quotes'] + pool_quotes = {code: curr_quotes[code] for code in my_suber.code_list if code in curr_quotes} + redis_execute_strategy(curr_date, curr_time, curr_seconds, pool_quotes) + print('!' if len(curr_quotes) > 0 else 'x', end='') # 这里确保有数据 + except Exception as e: + err = '[redis err] {}'.format(e) + print(err, end='') + + +def redis_unsubscribe(): + my_redis.unsubscribe(REDIS_CHANNEL) + print('[停止监听数据]') def redis_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quotes: Dict) -> None: - print('!' if len(curr_quotes) > 0 else 'x', end='') # 这里确保有数据 positions = my_delegate.check_positions() for time_range in SellConf.time_ranges: @@ -276,9 +281,10 @@ def redis_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu path_assets=PATH_ASSETS, execute_strategy=empty_execute_strategy, before_trade_day=before_trade_day, + custom_sub_begin=redis_subscribe, + custom_unsub_end=redis_unsubscribe, ding_messager=DING_MESSAGER, open_today_deal_report=True, open_today_hold_report=True, ) - threading.Thread(target=redis_subscriber).start() my_suber.start_scheduler() diff --git a/tools/utils_remote.py b/tools/utils_remote.py index c91b3d9..8cec846 100644 --- a/tools/utils_remote.py +++ b/tools/utils_remote.py @@ -540,7 +540,7 @@ def get_bao_daily_history( df['high'] = df['high'].astype(float) df['low'] = df['low'].astype(float) df['close'] = df['close'].astype(float) - df['volume'] = df['volume'].replace('', '0').astype(int) / 100 + df['volume'] = df['volume'].replace('', '0').astype(int) / 100 # 停牌的票会返回空串所以改成0 df['amount'] = df['amount'].replace('', '0').astype(float) if df is not None and len(df) > 0: From 3edce1c26c6db1b79b20c93c61fbd475ff90f970 Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:03:15 +0800 Subject: [PATCH 09/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AB=9E=E4=BB=B7?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E5=9B=9E=E8=B0=83=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8D=88=E7=9B=98=E6=80=BB=E7=BB=93=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- delegate/base_subscriber.py | 22 ++++++++++++++++++++-- delegate/xt_subscriber.py | 28 ++++++++++++++++++---------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/delegate/base_subscriber.py b/delegate/base_subscriber.py index d8b2843..4bc2b31 100644 --- a/delegate/base_subscriber.py +++ b/delegate/base_subscriber.py @@ -28,6 +28,7 @@ def __init__( path_assets: str, # 回调 execute_strategy: Callable, # 策略回调函数 + execute_call_end: Callable = None, # 策略竞价结束回调 execute_interval: int = 1, # 策略执行间隔,单位(秒) before_trade_day: Callable = None, # 盘前函数 near_trade_begin: Callable = None, # 盘后函数 @@ -38,6 +39,7 @@ def __init__( # 通知 ding_messager: BaseMessager = None, # 日报 + open_middle_end_report: bool = False, # 午盘结束的报告 open_today_deal_report: bool = False, # 每日交易记录报告 open_today_hold_report: bool = False, # 每日持仓记录报告 today_report_show_bank: bool = False, # 是否显示银行流水(国金QMT会卡死所以默认关闭) @@ -52,6 +54,7 @@ def __init__( self.path_assets = path_assets self.execute_strategy = execute_strategy + self.execute_call_end = execute_call_end self.execute_interval = execute_interval self.before_trade_day = before_trade_day # 提前准备某些耗时长的任务 self.near_trade_begin = near_trade_begin # 有些数据临近开盘才更新,这里保证内存里的数据正确 @@ -60,6 +63,7 @@ def __init__( self.custom_begin_sub = custom_sub_begin self.custom_end_unsub = custom_unsub_end + self.open_middle_end_report = open_middle_end_report self.open_today_deal_report = open_today_deal_report self.open_today_hold_report = open_today_hold_report self.today_report_show_bank = today_report_show_bank @@ -175,6 +179,14 @@ def finish_trade_day_wrapper(self): if self.finish_trade_day is not None: self.finish_trade_day() + def execute_call_end_wrapper(self): + if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): + return + + print('[竞价结束回调]') + if self.execute_call_end is not None: + self.execute_call_end() + # 检查是否完成盘前准备 def check_before_finished(self): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): @@ -263,13 +275,19 @@ def _start_scheduler(self): random_minute = random.randint(0, 10) + 5 self.scheduler.add_job(self.finish_trade_day_wrapper, 'cron', hour=16, minute=random_minute) - self.scheduler.add_job(self.prev_check_open_day, 'cron', hour=1, minute=0, second=0) + if self.execute_call_end is not None: + self.scheduler.add_job(self.execute_call_end_wrapper(), 'cron', hour=9, minute=25, second=30) + + if self.open_middle_end_report: + self.scheduler.add_job(self.daily_summary, 'cron', hour=11, minute=32) + + self.scheduler.add_job(self.prev_check_open_day, 'cron', hour=1, minute=0) self.scheduler.add_job(self.check_before_finished, 'cron', hour=8, minute=55) # 检查当天是否完成准备 self.scheduler.add_job(self.callback_open_no_quotes, 'cron', hour=9, minute=14, second=30) self.scheduler.add_job(self.callback_close_no_quotes, 'cron', hour=11, minute=30, second=30) self.scheduler.add_job(self.callback_open_no_quotes, 'cron', hour=12, minute=59, second=30) self.scheduler.add_job(self.callback_close_no_quotes, 'cron', hour=15, minute=0, second=30) - self.scheduler.add_job(self.daily_summary, 'cron', hour=15, minute=1, second=0) + self.scheduler.add_job(self.daily_summary, 'cron', hour=15, minute=2) try: print('[定时器已启动]') diff --git a/delegate/xt_subscriber.py b/delegate/xt_subscriber.py index f1594af..850db1b 100644 --- a/delegate/xt_subscriber.py +++ b/delegate/xt_subscriber.py @@ -29,6 +29,7 @@ def __init__( path_assets: str, # 回调 execute_strategy: Callable, # 策略回调函数 + execute_call_end: Callable = None, # 策略竞价结束回调 execute_interval: int = 1, # 策略执行间隔,单位(秒) before_trade_day: Callable = None, # 盘前函数 near_trade_begin: Callable = None, # 盘后函数 @@ -38,6 +39,7 @@ def __init__( # 通知 ding_messager: BaseMessager = None, # 日报 + open_middle_end_report: bool = False, # 午盘结束的报告 open_today_deal_report: bool = False, # 每日交易记录报告 open_today_hold_report: bool = False, # 每日持仓记录报告 today_report_show_bank: bool = False, # 是否显示银行流水(国金QMT会卡死所以默认关闭) @@ -52,10 +54,12 @@ def __init__( path_deal=path_deal, path_assets=path_assets, execute_strategy=execute_strategy, + execute_call_end=execute_call_end, execute_interval=execute_interval, before_trade_day=before_trade_day, near_trade_begin=near_trade_begin, finish_trade_day=finish_trade_day, + open_middle_end_report=open_middle_end_report, open_today_deal_report=open_today_deal_report, open_today_hold_report=open_today_hold_report, today_report_show_bank=today_report_show_bank, @@ -309,23 +313,26 @@ def _start_qmt_scheduler(self): ['15:01', self.unsubscribe_tick, None], ['15:02', self.daily_summary, None], ] + if self.open_tick: cron_jobs.append(['09:10', self.clean_ticks_history, None]) cron_jobs.append(['15:10', self.save_tick_history, None]) if self.before_trade_day is not None: - cron_jobs.append([ # 03:00 ~ 06:59 - f'0{random.randint(0, 3) + 3}:{random.randint(0, 59)}', - self.before_trade_day_wrapper, - None, - ]) # random 时间为了跑多个策略时防止短期预加载数据流量压力过大 + # random 时间为了跑多个策略时防止短期预加载数据流量压力过大 + before_time = f'0{random.randint(0, 3) + 3}:{random.randint(0, 59)}' # 03:00 ~ 06:59 + cron_jobs.append([before_time, self.before_trade_day_wrapper, None]) if self.finish_trade_day is not None: - cron_jobs.append([ # 16:05 ~ 16:15 - f'16:{random.randint(0, 10) + 5}', - self.finish_trade_day_wrapper, - None, - ]) + # random 时间为了跑多个策略时防止短期预加载数据流量压力过大 + finish_time = f'16:{random.randint(0, 10) + 5}' # 16:05 ~ 16:15 + cron_jobs.append([finish_time, self.finish_trade_day_wrapper, None]) + + if self.execute_call_end is not None: + cron_jobs.append(['09:26', self.execute_call_end_wrapper, None]) + + if self.open_middle_end_report: + cron_jobs.append(['11:32', self.daily_summary, None]) # 数据源中断检查时间点 monitor_time_list = [ @@ -334,6 +341,7 @@ def _start_qmt_scheduler(self): '13:05', '13:15', '13:25', '13:35', '13:45', '13:55', '14:05', '14:15', '14:25', '14:35', '14:45', '14:55', ] + if self.use_ap_scheduler: # 新版 apscheduler for cron_job in cron_jobs: From dc78fd92c0296bbc4318708698d5403f79b6fc17 Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:21:38 +0800 Subject: [PATCH 10/16] + little polish --- delegate/daily_reporter.py | 7 +++++-- delegate/xt_subscriber.py | 3 ++- run_redis_pull.py | 3 ++- tools/utils_remote.py | 3 ++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/delegate/daily_reporter.py b/delegate/daily_reporter.py index a7dae7c..489ad35 100644 --- a/delegate/daily_reporter.py +++ b/delegate/daily_reporter.py @@ -1,4 +1,5 @@ import os +import datetime from typing import Optional import pandas as pd @@ -29,11 +30,13 @@ def get_total_asset_increase(path_assets: str, curr_date: str, curr_asset: float df = pd.read_csv(path_assets) # 读取 prev_asset = df.tail(1)['asset'].values[0] # 获取最近的日期资产 df.loc[len(df)] = [curr_date, curr_asset] # 添加最新的日期资产 - df.to_csv(path_assets, index=False) # 存储 + if datetime.datetime.now().hour > 12: # 防止午盘重写 + df.to_csv(path_assets, index=False) # 存储 return curr_asset - prev_asset else: df = pd.DataFrame({'date': [curr_date], 'asset': [curr_asset]}) - df.to_csv(path_assets, index=False) + if datetime.datetime.now().hour > 12: # 防止午盘重写 + df.to_csv(path_assets, index=False) return None diff --git a/delegate/xt_subscriber.py b/delegate/xt_subscriber.py index 850db1b..c4f5d7a 100644 --- a/delegate/xt_subscriber.py +++ b/delegate/xt_subscriber.py @@ -98,7 +98,8 @@ def __init__( + [f'askPrice{i}' for i in range(1, 6)] \ + [f'askVol{i}' for i in range(1, 6)] \ + [f'bidPrice{i}' for i in range(1, 6)] \ - + [f'bidVol{i}' for i in range(1, 6)] + + [f'bidVol{i}' for i in range(1, 6)] \ + + ['lastClose'] self.curr_trade_date = '1990-12-19' #记录当前股票交易日期 diff --git a/run_redis_pull.py b/run_redis_pull.py index 70f93ec..f9b0b8b 100644 --- a/run_redis_pull.py +++ b/run_redis_pull.py @@ -179,6 +179,7 @@ def empty_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu def redis_subscribe(): print('[开始监听数据]') my_redis.subscribe(REDIS_CHANNEL) # 订阅频道 + my_code_set = set(my_pool.get_code_list()) # set 提高 in 操作的性能 O(1) 查找复杂度 for message in my_redis.listen(): try: if message['type'] == 'message': @@ -187,7 +188,7 @@ def redis_subscribe(): curr_time = data['curr_time'] curr_seconds = data['curr_seconds'] curr_quotes = data['curr_quotes'] - pool_quotes = {code: curr_quotes[code] for code in my_suber.code_list if code in curr_quotes} + pool_quotes = {code: curr_quotes[code] for code in my_code_set if code in curr_quotes} redis_execute_strategy(curr_date, curr_time, curr_seconds, pool_quotes) print('!' if len(curr_quotes) > 0 else 'x', end='') # 这里确保有数据 except Exception as e: diff --git a/tools/utils_remote.py b/tools/utils_remote.py index 8cec846..c196183 100644 --- a/tools/utils_remote.py +++ b/tools/utils_remote.py @@ -191,9 +191,10 @@ def _adjust_list(input_list: list, target_length: int) -> list: return adjusted -def qmt_quote_to_tick(quote: dict): +def qmt_quote_to_tick(quote: dict) -> dict: ans = { 'time': datetime.datetime.fromtimestamp(quote['time'] / 1000).strftime('%H:%M:%S'), + 'lastClose': quote['lastClose'], 'price': quote['lastPrice'], 'high': quote['high'], 'low': quote['low'], From 76546e044260e932d87a85d030c4b977993cc4af Mon Sep 17 00:00:00 2001 From: dominicx Date: Thu, 18 Dec 2025 22:24:47 +0800 Subject: [PATCH 11/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=8A=E4=BA=A455?= =?UTF-8?q?=E5=BC=80=E5=A4=B4ETF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 科创债ETF --- tools/utils_basic.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/utils_basic.py b/tools/utils_basic.py index 107a5b2..a4cebc2 100644 --- a/tools/utils_basic.py +++ b/tools/utils_basic.py @@ -64,7 +64,7 @@ def symbol_to_code(symbol: str | int) -> str: if symbol[:2] in ['00', '30', '15', '12']: return f'{symbol}.SZ' - elif symbol[:2] in ['60', '68', '51', '52', '53', '56', '58', '11']: + elif symbol[:2] in ['60', '68', '51', '52', '53', '55', '56', '58', '11']: return f'{symbol}.SH' elif symbol[:2] in ['83', '87', '43', '82', '88', '92']: return f'{symbol}.BJ' @@ -162,7 +162,7 @@ def symbol_to_gmsymbol(symbol: str | int) -> str: if symbol[:2] in ['00', '30', '15', '12']: return f'SZSE.{symbol}' - elif symbol[:2] in ['60', '68', '51', '52', '53', '56', '58', '11']: + elif symbol[:2] in ['60', '68', '51', '52', '53', '55', '56', '58', '11']: return f'SHSE.{symbol}' elif symbol[:2] in ['83', '87', '43', '82', '88', '92']: return f'BJSE.{symbol}' @@ -190,7 +190,7 @@ def is_symbol(code_or_symbol: str): '00', '30', # 深交所 '60', '68', # 上交所 '82', '83', '87', '88', '43', '92', # 北交所 - '15', '51', '52', '53', '56', '58', # ETF + '15', '51', '52', '53', '55', '56', '58', # ETF '11', '12', # 可转债 ] @@ -240,7 +240,7 @@ def is_stock_bj(code_or_symbol: str | int): def is_fund_etf(code_or_symbol: str | int): """ 判断是不是etf代码 """ code_or_symbol = str(code_or_symbol) if type(code_or_symbol) == int else code_or_symbol - return code_or_symbol[:2] in ['15', '51', '52', '53', '56', '58'] + return code_or_symbol[:2] in ['15', '51', '52', '53', '55', '56', '58'] def is_bond(code_or_symbol: str | int): @@ -253,7 +253,7 @@ def is_bond(code_or_symbol: str | int): def get_symbol_exchange(symbol: str) -> str: if symbol[:2] in ['00', '30', '15', '12']: return 'SZ' - elif symbol[:2] in ['60', '68', '51', '52', '53', '56', '58', '11']: + elif symbol[:2] in ['60', '68', '51', '52', '53', '55', '56', '58', '11']: return 'SH' elif symbol[:2] in ['83', '87', '43', '82', '88', '92']: return 'BJ' @@ -488,4 +488,4 @@ def convert_daily_to_monthly(df: pd.DataFrame) -> pd.DataFrame: for col in monthly_data.columns: monthly_data[col] = monthly_data[col].astype(df[col].dtype) - return monthly_data \ No newline at end of file + return monthly_data From 8458db4fa4ba17362c668fdcc6649e14d999c69b Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Thu, 25 Dec 2025 18:39:34 +0800 Subject: [PATCH 12/16] =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=8F=B0=E6=96=87?= =?UTF-8?q?=E6=A1=88=E8=BE=93=E5=87=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _doc/CHANGELOG.md | 5 ++ delegate/base_subscriber.py | 51 ++++++------ delegate/daily_history.py | 42 +++++----- delegate/xt_subscriber.py | 159 ++++++++++++++++-------------------- run_ai_gen.py | 4 +- run_redis_pull.py | 6 +- run_redis_push.py | 2 +- run_remote.py | 4 +- run_shield.py | 4 +- run_swords.py | 4 +- run_wencai_qmt.py | 4 +- run_wencai_tdx.py | 4 +- tools/utils_cache.py | 4 +- tools/utils_ding.py | 8 +- tools/utils_remote.py | 2 +- trader/pools.py | 12 +-- trader/seller_components.py | 55 ++++++++++--- trader/seller_groups.py | 3 +- 18 files changed, 193 insertions(+), 180 deletions(-) diff --git a/_doc/CHANGELOG.md b/_doc/CHANGELOG.md index 3942081..0713e6b 100644 --- a/_doc/CHANGELOG.md +++ b/_doc/CHANGELOG.md @@ -5,10 +5,15 @@ ## [ In Progress ] ### 添加 +- 临跌停卖点模块,用以预防跌停被锁死的极端风险 +- 竞价结束callback接口 ### 修改 +- 部分文案优化 +- Subscriber代码拆分重构 ### 删除 +- 旧版schedule移除 ## [ 4.2.0 ] 2025-11-18 diff --git a/delegate/base_subscriber.py b/delegate/base_subscriber.py index 4bc2b31..5d22067 100644 --- a/delegate/base_subscriber.py +++ b/delegate/base_subscriber.py @@ -71,7 +71,6 @@ def __init__( self.messager = ding_messager self.scheduler = None - self.use_ap_scheduler = True # 如果use_outside_data 被设置为True,则需强制使用apscheduler self.create_scheduler() self.stock_names = StockNames() @@ -128,10 +127,10 @@ def callback_open_no_quotes(self): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): return - print('[启动策略]') - if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:开启') + self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:开启', output='[MSG START]\n') + + print('[启动策略]') if self.custom_begin_sub is not None: threading.Thread(target=self.custom_begin_sub).start() @@ -143,7 +142,7 @@ def callback_close_no_quotes(self): print('\n[关闭策略]') if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:结束') + self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:结束', output='[MSG STOP]\n') if self.custom_end_unsub is not None: threading.Thread(target=self.custom_end_unsub).start() @@ -300,30 +299,28 @@ def _start_scheduler(self): self.delegate.shutdown() def create_scheduler(self): - if self.use_ap_scheduler: - from apscheduler.schedulers.blocking import BlockingScheduler - from apscheduler.executors.pool import ThreadPoolExecutor - executors = { - 'default': ThreadPoolExecutor(32), - } - job_defaults = { - 'coalesce': True, - 'misfire_grace_time': 180, - 'max_instances': 3 - } - self.scheduler = BlockingScheduler(timezone='Asia/Shanghai', executors=executors, job_defaults=job_defaults) + from apscheduler.schedulers.blocking import BlockingScheduler + from apscheduler.executors.pool import ThreadPoolExecutor + executors = { + 'default': ThreadPoolExecutor(32), + } + job_defaults = { + 'coalesce': True, + 'misfire_grace_time': 180, + 'max_instances': 3 + } + self.scheduler = BlockingScheduler(timezone='Asia/Shanghai', executors=executors, job_defaults=job_defaults) def start_scheduler(self): - if self.use_ap_scheduler: - temp_now = datetime.datetime.now() - temp_date = temp_now.strftime('%Y-%m-%d') - temp_time = temp_now.strftime('%H:%M') - # 盘中执行需要补齐 - if '08:05' < temp_time < '15:30' and check_is_open_day(temp_date): - self.before_trade_day_wrapper() - self.near_trade_begin_wrapper() - if '09:15' < temp_time < '11:30' or '13:00' <= temp_time < '14:57': - self.callback_open_no_quotes() # 重启时如果在交易时间则订阅Tick + temp_now = datetime.datetime.now() + temp_date = temp_now.strftime('%Y-%m-%d') + temp_time = temp_now.strftime('%H:%M') + # 盘中执行需要补齐 + if '08:05' < temp_time < '15:30' and check_is_open_day(temp_date): + self.before_trade_day_wrapper() + self.near_trade_begin_wrapper() + if '09:15' < temp_time < '11:30' or '13:00' <= temp_time < '14:57': + self.callback_open_no_quotes() # 重启时如果在交易时间则订阅Tick self._start_scheduler() diff --git a/delegate/daily_history.py b/delegate/daily_history.py index 2eec35b..ae3909f 100644 --- a/delegate/daily_history.py +++ b/delegate/daily_history.py @@ -71,7 +71,7 @@ def get_subset_copy(self, codes: list[str], days: int) -> dict[str, pd.DataFrame if code in self.cache_history: i += 1 ans[code] = self[code].tail(days).copy() - print(f'[HISTORY] Find {i}/{len(codes)} codes returned.') + print(f'[历史日线] Find {i}/{len(codes)} codes returned.') return ans # 获取代码列表 @@ -85,7 +85,7 @@ def get_code_list(self, force_download: bool = False, prefixes: set[str] = None) df = AKCache.stock_info_a_code_name() df.to_csv(code_list_path, index=False) except Exception as e: - print('[HISTORY] Download code list failed! ', e) + print('[历史日线] Download code list failed! ', e) # 获取本地列表时 prefix 生效 if os.path.exists(code_list_path): @@ -132,33 +132,33 @@ def _download_codes(self, code_list: list[str], day_count: int) -> None: df.to_csv(f'{self.root_path}/{self.default_kline_folder}/{code}.csv', index=False) downloaded_count += 1 - print(f'[HISTORY] [{downloaded_count}/{min(i + group_size, len(code_list))}]', group_codes) + print(f'[历史日线] [{downloaded_count}/{min(i + group_size, len(code_list))}]', group_codes) # 有可能是当天新股没有数据,下载失败也正常 - print(f'[HISTORY] Download finished with {len(download_failure)} fails: {download_failure}') + print(f'[历史日线] Download finished with {len(download_failure)} fails: {download_failure}') # 自动补全本地缺失股票代码 def _download_local_missed(self): code_list = self.get_code_list() - print(f'[HISTORY] Checking local missed codes from {len(code_list)}...') + print(f'[历史日线] Checking local missed codes from {len(code_list)}...') missing_codes = [] for code in code_list: path = f'{self.root_path}/{self.default_kline_folder}/{code}.csv' if not os.path.exists(path): missing_codes.append(code) - print(f'[HISTORY] Downloading missing {len(missing_codes)} codes...') + print(f'[历史日线] Downloading missing {len(missing_codes)} codes...') self._download_codes(missing_codes, self.init_day_count) # 下载本地缺失的股票代码数据 def _download_remote_missed(self) -> None: - print('[HISTORY] Searching local missed code...') + print('[历史日线] Searching local missed code...') prev_code_list = self.get_code_list() curr_code_list = self.get_code_list(force_download=True) gap_codes = [] for code in curr_code_list: if code not in prev_code_list: gap_codes.append(code) - print(f'[HISTORY] Downloading {len(gap_codes)} gap codes data of {self.init_day_count} days...') + print(f'[历史日线] Downloading {len(gap_codes)} gap codes data of {self.init_day_count} days...') self._download_codes(gap_codes, self.init_day_count) # ============== @@ -173,7 +173,7 @@ def load_history_from_disk_to_memory(self, auto_update: bool = True) -> None: if auto_update: self._download_local_missed() - print(f'[HISTORY] Loading {len(code_list)} codes...', end='') + print(f'[历史日线] Loading {len(code_list)} codes...', end='') self.cache_history.clear() error_count = 0 i = 0 @@ -188,11 +188,11 @@ def load_history_from_disk_to_memory(self, auto_update: bool = True) -> None: except Exception as e: print(code, e) error_count += 1 - print(f'\n[HISTORY] Loading finished with {error_count}/{i} errors') + print(f'\n[历史日线] Loading finished with {error_count}/{i} errors') def download_all_to_disk(self, renew_code_list: bool = True) -> None: code_list = self.get_code_list(force_download=renew_code_list) - print(f'[HISTORY] Downloading all {len(code_list)} codes data of {self.init_day_count} days...') + print(f'[历史日线] Downloading all {len(code_list)} codes data of {self.init_day_count} days...') self._download_codes(code_list, self.init_day_count) # ============== @@ -202,7 +202,7 @@ def download_all_to_disk(self, renew_code_list: bool = True) -> None: # 下载具体某天的数据 TUSHARE def _update_codes_by_tushare(self, target_date: str, code_list: list[str]) -> set[str]: target_date_int = int(target_date) - print(f'[HISTORY] Updating {target_date} ', end='') + print(f'[历史日线] Updating {target_date} ', end='') loss_list = [] # 找到缺失当天数据的codes for code in code_list: @@ -243,13 +243,13 @@ def _update_codes_one_by_one(self, days: int, code_list: list[str]) -> set[str]: now = datetime.datetime.now() start_date = get_prev_trading_date(now, days) end_date = get_prev_trading_date(now, 1) - print(f'[HISTORY] Updating {start_date} - {end_date}', end='') + print(f'[历史日线] Updating {start_date} - {end_date}', end='') updated_codes = set() updated_count = 0 group_size = 100 for i in range(0, len(code_list), group_size): - print(f'\n[HISTORY] [{min(i + group_size, len(code_list))}]', end='') + print(f'\n[历史日线] [{min(i + group_size, len(code_list))}]', end='') group_codes = [sub_code for sub_code in code_list[i:i + group_size]] for code in group_codes: df = get_daily_history( @@ -287,7 +287,7 @@ def download_single_daily(self, target_date: str) -> None: self.load_history_from_disk_to_memory() code_list = self.get_code_list() updated_codes = self._update_codes_by_tushare(target_date, code_list) - print('[HISTORY] Sort and Save all history data ', end='') + print('[历史日线] Sort and Save all history data ', end='') i = 0 for code in updated_codes: i += 1 @@ -295,7 +295,7 @@ def download_single_daily(self, target_date: str) -> None: print('.', end='') self.cache_history[code] = self[code].sort_values(by='datetime') self.cache_history[code].to_csv(f'{self.root_path}/{self.default_kline_folder}/{code}.csv', index=False) - print(f'\n[HISTORY] Finished with {i} files updated') + print(f'\n[历史日线] Finished with {i} files updated') # 更新近几日数据,不用全部下载,速度快也不容易被Ban IP def download_recent_daily(self, days: int) -> None: @@ -321,7 +321,7 @@ def download_recent_daily(self, days: int) -> None: all_updated_codes = self._update_codes_one_by_one(days, code_list) # 排序存储所有更新过的数据 - print('[HISTORY] Sorting and Saving all history data ', end='') + print('[历史日线] Sorting and Saving all history data ', end='') i = 0 for code in all_updated_codes: i += 1 @@ -329,7 +329,7 @@ def download_recent_daily(self, days: int) -> None: print('.', end='') self.cache_history[code] = self[code].sort_values(by='datetime') self.cache_history[code].to_csv(f'{self.root_path}/{self.default_kline_folder}/{code}.csv', index=False) - print(f'\n[HISTORY] Finished with {i} files updated') + print(f'\n[历史日线] Finished with {i} files updated') self.write_last_update_datetime() @@ -363,10 +363,10 @@ def remove_single_history(self, code: str) -> bool: os.remove(file_path) return True except PermissionError: - print(f'[HISTORY] No Permission deleting {file_path}') + print(f'[历史日线] No Permission deleting {file_path}') return False except OSError as e: - print(f'[HISTORY] Error when deleting: {e}') + print(f'[历史日线] Error when deleting: {e}') return False @staticmethod @@ -388,4 +388,4 @@ def remove_recent_exit_right_histories(self, days: int) -> None: for code in codes: if self.remove_single_history(code): removed_count += 1 - print(f'[HISTORY] Removed {removed_count} histories with Exit Right announced') + print(f'[历史日线] Removed {removed_count} histories with Exit Right announced') diff --git a/delegate/xt_subscriber.py b/delegate/xt_subscriber.py index 6c7663d..e82e3c2 100644 --- a/delegate/xt_subscriber.py +++ b/delegate/xt_subscriber.py @@ -35,7 +35,7 @@ def __init__( near_trade_begin: Callable = None, # 盘后函数 finish_trade_day: Callable = None, # 盘后函数 # 订阅 - use_ap_scheduler: bool = False, # 默认使用旧版 schedule (尽可能向前兼容旧策略吧) + use_ap_scheduler: bool = True, # (已弃用)默认使用旧版 schedule # 通知 ding_messager: BaseMessager = None, # 日报 @@ -75,6 +75,7 @@ def __init__( self.lock_quotes_update = threading.Lock() # 聚合实时打点缓存的锁 self.cache_quotes: Dict[str, Dict] = {} # 记录实时的价格信息 + self.sub_sequence: int = -1 # 记录实时数据订阅号 self.code_list = ['000001.SH'] # 默认只有上证指数 self.last_callback_time = datetime.datetime.now() # 上次返回quotes 时间 @@ -94,12 +95,11 @@ def __init__( ) if self.is_ticks_df: - self.tick_df_cols = ['time', 'price', 'high', 'low', 'volume', 'amount'] \ + self.tick_df_cols = ['local', 'time', 'price', 'high', 'low', 'lastClose', 'volume', 'amount'] \ + [f'askPrice{i}' for i in range(1, 6)] \ + [f'askVol{i}' for i in range(1, 6)] \ + [f'bidPrice{i}' for i in range(1, 6)] \ - + [f'bidVol{i}' for i in range(1, 6)] \ - + ['lastClose'] + + [f'bidVol{i}' for i in range(1, 6)] self.curr_trade_date = '1990-12-19' #记录当前股票交易日期 @@ -174,36 +174,42 @@ def subscribe_tick(self, resume: bool = False): return if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:' - f'{"恢复" if resume else "开启"} {len(self.code_list)}支') - print('[开启行情订阅]', end='') + self.messager.send_text_as_md( + f'[{self.account_id}]{self.strategy_name}:{"恢复" if resume else "开启"} ' + f'{len(self.code_list)}支', + output='[Message] BEGIN SUBSCRIBING\n') xtdata.enable_hello = False - self.cache_limits['sub_seq'] = xtdata.subscribe_whole_quote(self.code_list, callback=self.callback_sub_whole) + self.sub_sequence = xtdata.subscribe_whole_quote(self.code_list, callback=self.callback_sub_whole) + print(f'[开启行情订阅] 订阅号:{self.sub_sequence}', end='') def unsubscribe_tick(self, pause: bool = False): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): return - if 'sub_seq' in self.cache_limits: - xtdata.unsubscribe_quote(self.cache_limits['sub_seq']) - print('\n[结束行情订阅]') + if self.sub_sequence > 0: + xtdata.unsubscribe_quote(self.sub_sequence) + print(f'\n[结束行情订阅] 订阅号:{self.sub_sequence}') if self.messager is not None: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:' - f'{"暂停" if pause else "关闭"}') + self.messager.send_text_as_md( + f'[{self.account_id}]{self.strategy_name}:{"暂停" if pause else "关闭"}', + output='[Message] END UNSUBSCRIBING\n') def resubscribe_tick(self, notice: bool = False): if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')): return - if 'sub_seq' in self.cache_limits: - xtdata.unsubscribe_quote(self.cache_limits['sub_seq']) - self.cache_limits['sub_seq'] = xtdata.subscribe_whole_quote(self.code_list, callback=self.callback_sub_whole) + prev_sub_sequence = None + if self.sub_sequence > 0: + prev_sub_sequence = self.sub_sequence + xtdata.unsubscribe_quote(self.sub_sequence) xtdata.enable_hello = False + self.sub_sequence = xtdata.subscribe_whole_quote(self.code_list, callback=self.callback_sub_whole) if self.messager is not None and notice: - self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:' - f'重启 {len(self.code_list)}支') - print('\n[重启行情订阅]', end='') + self.messager.send_text_as_md( + f'[{self.account_id}]{self.strategy_name}:重启 {len(self.code_list)}支', + output='[Message] FINISH RESUBSCRIBING\n') + print(f'\n[重启行情订阅] 订阅号:{prev_sub_sequence} -> {self.sub_sequence}', end='') def update_code_list(self, code_list: list[str]): # 加上证指数防止没数据不打点 @@ -218,9 +224,11 @@ def update_code_list(self, code_list: list[str]): def record_tick_to_memory(self, quotes): # 记录 tick 历史到内存 if self.is_ticks_df: + local_time = datetime.datetime.now().strftime('%H:%M:%S') for code in quotes: quote = quotes[code] tick = qmt_quote_to_tick(quote) + tick['local'] = local_time new_tick_df = pd.DataFrame([tick], columns=self.tick_df_cols) if code not in self.today_ticks: self.today_ticks[code] = new_tick_df @@ -280,7 +288,6 @@ def check_before_finished(self): or len(self.cache_history) < 1 ): print('[警告] 盘前准备未完成,尝试重新执行盘前函数') - self.prev_check_open_day() self.before_trade_day_wrapper() self.near_trade_begin_wrapper() print(f'[提示] 当前交易日:{self.curr_trade_date}') @@ -336,6 +343,17 @@ def _start_qmt_scheduler(self): if self.open_middle_end_report: cron_jobs.append(['11:32', self.daily_summary, None]) + # 新版 apscheduler + for cron_job in cron_jobs: + [hr, mn] = cron_job[0].split(':') + if cron_job[2] is None: + self.scheduler.add_job(cron_job[1], 'cron', hour=hr, minute=mn) + else: + self.scheduler.add_job(cron_job[1], 'cron', hour=hr, minute=mn, args=list(cron_job[2])) + + # 尝试重新订阅 tick 数据,减少30分时无数据返回机率 + self.scheduler.add_job(self.resubscribe_tick, 'cron', hour=9, minute=29, second=30) + # 数据源中断检查时间点 monitor_time_list = [ '09:35', '09:45', '09:55', '10:05', '10:15', '10:25', @@ -344,78 +362,41 @@ def _start_qmt_scheduler(self): '14:05', '14:15', '14:25', '14:35', '14:45', '14:55', ] - if self.use_ap_scheduler: - # 新版 apscheduler - for cron_job in cron_jobs: - [hr, mn] = cron_job[0].split(':') - if cron_job[2] is None: - self.scheduler.add_job(cron_job[1], 'cron', hour=hr, minute=mn) - else: - self.scheduler.add_job(cron_job[1], 'cron', hour=hr, minute=mn, args=list(cron_job[2])) - - # 尝试重新订阅 tick 数据,减少30分时无数据返回机率 - self.scheduler.add_job(self.resubscribe_tick, 'cron', hour=9, minute=29, second=30) - - for monitor_time in monitor_time_list: - [hr, mn] = monitor_time.split(':') - self.scheduler.add_job(self.callback_monitor, 'cron', hour=hr, minute=mn) - - # 启动定时器 + for monitor_time in monitor_time_list: + [hr, mn] = monitor_time.split(':') + self.scheduler.add_job(self.callback_monitor, 'cron', hour=hr, minute=mn) + + # 启动定时器 + try: + print('[定时进程] 任务启动') + self.scheduler.start() + except KeyboardInterrupt: + print('[定时进程] 手动结束') + except Exception as e: + print('[定时进程] 任务出错:', e) + finally: + self.delegate.shutdown() + print('[定时进程] 关闭完成') try: - print('[定时器已启动]') - self.scheduler.start() - except KeyboardInterrupt: - print('[手动结束进程]') - except Exception as e: - print('策略定时器出错:', e) - finally: - self.delegate.shutdown() - try: - import sys - sys.exit(0) - except SystemExit: - import os - os._exit(0) - else: - # 旧版 schedule - import schedule - for cron_job in cron_jobs: - if cron_job[2] is None: - schedule.every().day.at(cron_job[0]).do(cron_job[1]) - else: - schedule.every().day.at(cron_job[0]).do(cron_job[1], list(cron_job[2])[0]) - - for monitor_time in monitor_time_list: - schedule.every().day.at(monitor_time).do(self.callback_monitor) - - # 盘中执行需要补齐,旧代码都放在策略文件里了这里就不重复执行破坏老代码 - # if '08:05' < temp_time < '15:30' and check_is_open_day(temp_date): - # self._before_trade_day() - # if '09:15' < temp_time < '11:30' or '13:00' <= temp_time < '14:57': - # self.subscribe_tick() # 重启时如果在交易时间则订阅Tick - - # 旧代码还有别的要执行,没有放在 before_trade_day 所以这里虽然不优雅单也先注释掉 - # try: - # while True: - # schedule.run_pending() - # time.sleep(1) - # except KeyboardInterrupt: - # print('[手动结束进程]') - # finally: - # schedule.clear() - # self.delegate.shutdown() + import sys + sys.exit(0) + except SystemExit: + import os + os._exit(0) + def start_scheduler(self): - if self.use_ap_scheduler: - temp_now = datetime.datetime.now() - temp_date = temp_now.strftime('%Y-%m-%d') - temp_time = temp_now.strftime('%H:%M') - # 盘中执行需要补齐 - if '08:05' < temp_time < '15:30' and check_is_open_day(temp_date): - self.before_trade_day_wrapper() - self.near_trade_begin_wrapper() - if '09:15' < temp_time < '11:30' or '13:00' <= temp_time < '14:57': - self.subscribe_tick() # 重启时如果在交易时间则订阅Tick + temp_now = datetime.datetime.now() + temp_date = temp_now.strftime('%Y-%m-%d') + temp_time = temp_now.strftime('%H:%M') + # 盘中执行需要补齐 + if '08:05' < temp_time < '15:30' and check_is_open_day(temp_date): + self.prev_check_open_day() + self.before_trade_day_wrapper() + self.near_trade_begin_wrapper() + if '09:15' < temp_time < '11:30' or '13:00' <= temp_time < '14:57': + self.subscribe_tick() # 重启时如果在交易时间则订阅Tick + self._start_qmt_scheduler() diff --git a/run_ai_gen.py b/run_ai_gen.py index d8682e3..5b65b20 100644 --- a/run_ai_gen.py +++ b/run_ai_gen.py @@ -99,7 +99,7 @@ def before_trade_day() -> None: update_position_held(disk_lock, my_delegate, PATH_HELD) if all_held_inc(disk_lock, PATH_HELD): logging.warning('===== 所有持仓计数 +1 =====') - print(f'All held stock day +1!') + print(f'[持仓计数] All stocks held day +1') # refresh_code_list() -> None: my_pool.refresh() @@ -244,7 +244,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" - print(f'正在启动 {STRATEGY_NAME}...') + print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback from delegate.xt_delegate import XtDelegate diff --git a/run_redis_pull.py b/run_redis_pull.py index f9b0b8b..b4804b7 100644 --- a/run_redis_pull.py +++ b/run_redis_pull.py @@ -100,7 +100,7 @@ def before_trade_day() -> None: update_position_held(disk_lock, my_delegate, PATH_HELD) if all_held_inc(disk_lock, PATH_HELD): logging.warning('===== 所有持仓计数 +1 =====') - print(f'All held stock day +1!') + print(f'[持仓计数] All stocks held day +1') my_pool.refresh() positions = my_delegate.check_positions() @@ -179,7 +179,7 @@ def empty_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu def redis_subscribe(): print('[开始监听数据]') my_redis.subscribe(REDIS_CHANNEL) # 订阅频道 - my_code_set = set(my_pool.get_code_list()) # set 提高 in 操作的性能 O(1) 查找复杂度 + my_code_set = set(my_suber.code_list) # set 提高 in 操作的性能 O(1) 查找复杂度 for message in my_redis.listen(): try: if message['type'] == 'message': @@ -217,7 +217,7 @@ def redis_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" - print(f'正在启动 {STRATEGY_NAME}...') + print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback from delegate.xt_delegate import XtDelegate diff --git a/run_redis_push.py b/run_redis_push.py index 716a6e1..4be229f 100644 --- a/run_redis_push.py +++ b/run_redis_push.py @@ -61,7 +61,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" - print(f'正在启动 {STRATEGY_NAME}...') + print(f'[正在启动] {STRATEGY_NAME}') my_redis = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0) my_pool = Pool( account_id=QMT_ACCOUNT_ID, diff --git a/run_remote.py b/run_remote.py index 148c77a..7fe2ee8 100644 --- a/run_remote.py +++ b/run_remote.py @@ -98,7 +98,7 @@ def before_trade_day() -> None: update_position_held(disk_lock, my_delegate, PATH_HELD) if all_held_inc(disk_lock, PATH_HELD): logging.warning('===== 所有持仓计数 +1 =====') - print(f'All held stock day +1!') + print(f'[持仓计数] All stocks held day +1') # refresh_code_list() -> None: my_pool.refresh() @@ -229,7 +229,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" - print(f'正在启动 {STRATEGY_NAME}...') + print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback from delegate.xt_delegate import XtDelegate diff --git a/run_shield.py b/run_shield.py index 9fcad98..b4c6767 100644 --- a/run_shield.py +++ b/run_shield.py @@ -71,7 +71,7 @@ def before_trade_day() -> None: update_position_held(disk_lock, my_delegate, PATH_HELD) if all_held_inc(disk_lock, PATH_HELD): logging.warning('===== 所有持仓计数 +1 =====') - print(f'All held stock day +1!') + print(f'[持仓计数] All stocks held day +1') # refresh_code_list() -> None: my_pool.refresh() @@ -108,7 +108,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" - print(f'正在启动 {STRATEGY_NAME}...') + print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback from delegate.xt_delegate import XtDelegate diff --git a/run_swords.py b/run_swords.py index c627fd4..dcc5484 100644 --- a/run_swords.py +++ b/run_swords.py @@ -75,7 +75,7 @@ def before_trade_day() -> None: update_position_held(disk_lock, my_delegate, PATH_HELD) if all_held_inc(disk_lock, PATH_HELD): logging.warning('===== 所有持仓计数 +1 =====') - print(f'All held stock day +1!') + print(f'[持仓计数] All stocks held day +1') # refresh_code_list() -> None: my_pool.refresh() @@ -204,7 +204,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" - print(f'正在启动 {STRATEGY_NAME}...') + print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback from delegate.xt_delegate import XtDelegate diff --git a/run_wencai_qmt.py b/run_wencai_qmt.py index 6bb2f5b..4e00ba5 100644 --- a/run_wencai_qmt.py +++ b/run_wencai_qmt.py @@ -102,7 +102,7 @@ def before_trade_day() -> None: update_position_held(disk_lock, my_delegate, PATH_HELD) if all_held_inc(disk_lock, PATH_HELD): logging.warning('===== 所有持仓计数 +1 =====') - print(f'All held stock day +1!') + print(f'[持仓计数] All stocks held day +1') # refresh_code_list() -> None: my_pool.refresh() @@ -198,7 +198,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" - print(f'正在启动 {STRATEGY_NAME}...') + print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback from delegate.xt_delegate import XtDelegate diff --git a/run_wencai_tdx.py b/run_wencai_tdx.py index c8a11e9..30e34fe 100644 --- a/run_wencai_tdx.py +++ b/run_wencai_tdx.py @@ -102,7 +102,7 @@ def before_trade_day() -> None: update_position_held(disk_lock, my_delegate, PATH_HELD) if all_held_inc(disk_lock, PATH_HELD): logging.warning('===== 所有持仓计数 +1 =====') - print(f'All held stock day +1!') + print(f'[持仓计数] All stocks held day +1') # refresh_code_list() -> None: my_pool.refresh() @@ -196,7 +196,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" - print(f'正在启动 {STRATEGY_NAME}...') + print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback from delegate.xt_delegate import XtDelegate diff --git a/tools/utils_cache.py b/tools/utils_cache.py index 4de72db..763cb63 100644 --- a/tools/utils_cache.py +++ b/tools/utils_cache.py @@ -551,7 +551,7 @@ def check_is_open_day_sina(curr_date: str) -> bool: if curr_year <= trade_day_cache[trade_max_year_key]: # 未过期 ans = curr_date in trade_day_list trade_day_cache[curr_date] = ans - print(f'[{curr_date} is {ans} trade day in memory]') + print(f'[文件缓存] {curr_date} is {ans} trade day') return ans # 网络缓存 @@ -563,7 +563,7 @@ def check_is_open_day_sina(curr_date: str) -> bool: if curr_year <= trade_day_cache[trade_max_year_key]: # 未过期 ans = curr_date in trade_day_list trade_day_cache[curr_date] = ans - print(f'[{curr_date} is {ans} trade day in memory]') + print(f'[网络缓存] {curr_date} is {ans} trade day') return ans # 实在拿不到数据默认为True diff --git a/tools/utils_ding.py b/tools/utils_ding.py index e1c575f..d63ea7d 100644 --- a/tools/utils_ding.py +++ b/tools/utils_ding.py @@ -93,10 +93,10 @@ def send_text(self, text: str, output: str = '', alert: bool = False) -> bool: if len(output) > 0: print(output, end='') else: - print('Ding message send success!') + print('[Ding] message send success!') return True else: - print('Ding message send failed: ', res['errmsg']) + print('[Ding] message send failed: ', res['errmsg']) return False def send_text_as_md(self, text: str, output: str = '', alert: bool = False) -> bool: @@ -136,8 +136,8 @@ def send_markdown(self, title: str, text: str, output: str = '', alert: bool = F if len(output) > 0: print(output, end='') else: - print('Ding markdown send success!') + print('[Ding] markdown send success!') return True else: - print('Ding markdown send failed: ', res['errmsg']) + print('[Ding] markdown send failed: ', res['errmsg']) return False diff --git a/tools/utils_remote.py b/tools/utils_remote.py index c196183..c5e5514 100644 --- a/tools/utils_remote.py +++ b/tools/utils_remote.py @@ -237,7 +237,7 @@ def qmt_quote_to_day_kline(quote: dict, curr_date: str) -> dict: def concat_ak_quote_dict(source_df: pd.DataFrame, quote: dict, curr_date: str) -> pd.DataFrame: record = qmt_quote_to_day_kline(quote, curr_date=curr_date) new_row_df = pd.DataFrame([record.values()], columns=list(record.keys())) - return pd.concat([source_df, new_row_df], ignore_index=True) + return pd.concat([source_df, new_row_df], ignore_index=True) if len(source_df) > 0 else new_row_df def append_ak_daily_row(source_df: pd.DataFrame, row: dict) -> pd.DataFrame: diff --git a/trader/pools.py b/trader/pools.py index 00ca6bc..921d886 100644 --- a/trader/pools.py +++ b/trader/pools.py @@ -37,9 +37,9 @@ def refresh(self): self.refresh_white() self.update_code_list() - print(f'[POOL] White list refreshed {len(self.cache_whitelist)} codes.') - print(f'[POOL] Black list refreshed {len(self.cache_blacklist)} codes.') - print(f'[POOL] Total list refreshed {len(self.get_code_list())} codes.') + print(f'[监控股池] White list refreshed {len(self.cache_whitelist)} codes.') + print(f'[监控股池] Black list refreshed {len(self.cache_blacklist)} codes.') + print(f'[监控股池] Total list refreshed {len(self.get_code_list())} codes.') if self.messager is not None: self.messager.send_text_as_md( @@ -54,7 +54,7 @@ def refresh_white(self): # 删除不符合模式和没有缓存的票池 def filter_white_list_by_selector(self, filter_func: Callable, cache_history: dict[str, pd.DataFrame]): - print('[POOL] Filtering...', end='') + print('[监控股池] Filtering...', end='') i = 0 remove_list = [] @@ -68,7 +68,7 @@ def filter_white_list_by_selector(self, filter_func: Callable, cache_history: di if (len(df) > 0) and (not df['PASS'].values[-1]): remove_list.append(code) except Exception as e: - print(f'[POOL] Error and dropped {code} when filtering: ', e) + print(f'[监控股池] Error and dropped {code} when filtering: ', e) remove_list.append(code) else: remove_list.append(code) @@ -77,7 +77,7 @@ def filter_white_list_by_selector(self, filter_func: Callable, cache_history: di self.cache_whitelist.discard(code) self.update_code_list() - print(f'[POOL] {len(remove_list)} codes filter out, {len(self.get_code_list())} codes left.') + print(f'[监控股池] {len(remove_list)} codes filter out, {len(self.get_code_list())} codes left.') if self.messager is not None: self.messager.send_text_as_md(f'[{self.account_id}]{self.strategy_name}:筛除{len(remove_list)}支') diff --git a/trader/seller_components.py b/trader/seller_components.py index f0e098c..fe0ea30 100644 --- a/trader/seller_components.py +++ b/trader/seller_components.py @@ -5,7 +5,7 @@ from mytt.MyTT import MA, MACD, CCI, WR from xtquant.xttype import XtPosition -from tools.utils_basic import get_limit_up_price +from tools.utils_basic import get_limit_up_price, get_limiting_down_rate from tools.utils_remote import concat_ak_quote_dict from trader.seller import BaseSeller @@ -16,7 +16,7 @@ class HardSeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('硬性卖出策略', end=' ') + print('硬性卖点模块', end=' ') self.hard_time_range = parameters.hard_time_range self.earn_limit = parameters.earn_limit self.risk_limit = parameters.risk_limit @@ -42,13 +42,42 @@ def check_sell( return False +# -------------------------------- +# 临近跌停止损逻辑,第二个用以预防极端风险 +# -------------------------------- +class SafeSeller(BaseSeller): + def __init__(self, strategy_name, delegate, parameters): + BaseSeller.__init__(self, strategy_name, delegate, parameters) + print('临跌停卖点模块', end=' ') + self.stop_time_range = parameters.hard_time_range + self.safe_rate = parameters.safe_rate + + def check_sell( + self, code: str, quote: Dict, curr_date: str, curr_time: str, + position: XtPosition, held_day: int, max_price: Optional[float], + history: Optional[pd.DataFrame], ticks: Optional[list[list]], extra: any, + ) -> bool: + if (held_day > 0) and (self.stop_time_range[0] <= curr_time < self.stop_time_range[1]): + curr_price = quote['lastPrice'] + last_close = quote['lastClose'] + sell_volume = position.can_use_volume + + stop_rate = get_limiting_down_rate(code) + self.safe_rate + stop_price = last_close * stop_rate + + if curr_price <= stop_price: + self.order_sell(code, quote, sell_volume, f'临跌停{int((1 - stop_rate) * 100)}%') + return True + return False + + # -------------------------------- # 盈利未达预期则卖出换仓 # -------------------------------- class SwitchSeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('换仓卖出策略', end=' ') + print('换仓卖点模块', end=' ') self.switch_time_range = parameters.switch_time_range self.switch_hold_days = parameters.switch_hold_days self.switch_demand_daily_up = parameters.switch_demand_daily_up @@ -76,7 +105,7 @@ def check_sell( class FallSeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('回落卖出策略', end=' ') + print('回落卖点模块', end=' ') self.fall_time_range = parameters.fall_time_range self.fall_from_top = parameters.fall_from_top @@ -109,7 +138,7 @@ def check_sell( class ReturnSeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('回撤卖出策略', end=' ') + print('回撤卖点模块', end=' ') self.return_time_range = parameters.return_time_range self.return_of_profit = parameters.return_of_profit @@ -142,7 +171,7 @@ def check_sell( # class TailCapSeller(BaseSeller): # def __init__(self, strategy_name, delegate, parameters): # BaseSeller.__init__(self, strategy_name, delegate, parameters) -# print('尾盘涨停卖出策略', end=' ') +# print('尾盘涨停卖点模块', end=' ') # self.tail_time_range = parameters.tail_time_range # # def check_sell(self, code: str, quote: Dict, curr_date: str, curr_time: str, position: XtPosition, @@ -205,7 +234,7 @@ def check_sell( class MASeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print(f'跌破{parameters.ma_above}日均线卖出策略', end=' ') + print(f'跌破{parameters.ma_above}日均线卖点模块', end=' ') self.ma_time_range = parameters.ma_time_range self.ma_above = parameters.ma_above @@ -237,7 +266,7 @@ def check_sell( class CCISeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('CCI卖出策略', end=' ') + print('CCI卖点模块', end=' ') self.cci_time_range = parameters.cci_time_range self.cci_upper = parameters.cci_upper self.cci_lower = parameters.cci_lower @@ -272,7 +301,7 @@ def check_sell( class WRSeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('WR上穿卖出策略', end=' ') + print('WR上穿卖点模块', end=' ') self.wr_time_range = parameters.wr_time_range self.wr_cross = parameters.wr_cross @@ -302,7 +331,7 @@ def check_sell( class VolumeDropSeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('次缩卖出策略', end=' ') + print('次缩卖点模块', end=' ') self.next_time_range = parameters.next_time_range self.next_volume_dec_threshold = parameters.vol_dec_thre self.next_volume_dec_minute = parameters.vol_dec_time @@ -337,7 +366,7 @@ def check_sell( class DropSeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('高开出货卖出', end=' ') + print('高开出货卖点模块', end=' ') self.drop_time_range = parameters.drop_time_range self.drop_out_limits = parameters.drop_out_limits @@ -376,7 +405,7 @@ def check_sell( class IncBlocker(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('上涨过程禁卖', end=' ') + print('上涨过程禁卖模块', end=' ') def check_sell( self, code: str, quote: Dict, curr_date: str, curr_time: str, @@ -397,7 +426,7 @@ def check_sell( class UppingBlocker(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) - print('上行趋势禁卖', end=' ') + print('上行趋势禁卖模块', end=' ') def check_sell( self, code: str, quote: Dict, curr_date: str, curr_time: str, diff --git a/trader/seller_groups.py b/trader/seller_groups.py index 8f2bb65..63f500a 100644 --- a/trader/seller_groups.py +++ b/trader/seller_groups.py @@ -6,10 +6,11 @@ def __init__(self): pass def group_init(self, strategy_name, delegate, parameters): + print('[卖出策略] ', end='') for parent in self.__class__.__bases__: if parent.__name__ != 'GroupSellers': parent.__init__(self, strategy_name, delegate, parameters) - print('>> 初始化完成') + print('>> 组合完成') def group_check_sell( self, code: str, quote: Dict, curr_date: str, curr_time: str, From bf5c5869864c87662da975b0d9231d0d03699753 Mon Sep 17 00:00:00 2001 From: dominicx Date: Wed, 7 Jan 2026 23:47:27 +0800 Subject: [PATCH 13/16] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=9B=98=E5=90=8E?= =?UTF-8?q?=E6=B8=85=E7=82=B9=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 极端情况increase 返回None,会出现标题跟持仓市值 之间缺少MSG_OUTER_SEPARATOR,也就是’盘后清点MSG_INNER_SEPARATOR持仓市值这样。 --- delegate/daily_reporter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/delegate/daily_reporter.py b/delegate/daily_reporter.py index 489ad35..00a9735 100644 --- a/delegate/daily_reporter.py +++ b/delegate/daily_reporter.py @@ -149,12 +149,10 @@ def today_hold_report(self, today: str, positions): def check_asset(self, today: str, asset): title = f'[{self.account_id}]{self.strategy_name} 盘后清点' - text = title + text = title + MSG_OUTER_SEPARATOR increase = get_total_asset_increase(self.path_assets, today, asset.total_asset) if increase is not None: - text += MSG_OUTER_SEPARATOR - total_change = colour_text( f'{"+" if increase > 0 else ""}{round(increase, 2)}', increase > 0, From 63f5f02b7db86677e44193af9738a0394f77fb26 Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:10:36 +0800 Subject: [PATCH 14/16] =?UTF-8?q?=E6=96=87=E6=A1=88=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _doc/CHANGELOG.md | 4 ++++ delegate/base_subscriber.py | 10 +++++++--- delegate/daily_history.py | 7 ++++++- delegate/gm_callback.py | 34 +++++++++++++++++----------------- delegate/xt_subscriber.py | 16 +++++++++------- run_ai_gen.py | 2 +- run_redis_pull.py | 6 +++--- run_redis_push.py | 2 +- run_remote.py | 2 +- run_shield.py | 2 +- run_swords.py | 2 +- run_wencai_qmt.py | 2 +- run_wencai_tdx.py | 2 +- tools/utils_basic.py | 19 +++++++++++++------ tools/utils_cache.py | 2 +- 15 files changed, 67 insertions(+), 45 deletions(-) diff --git a/_doc/CHANGELOG.md b/_doc/CHANGELOG.md index 0713e6b..68dd6c8 100644 --- a/_doc/CHANGELOG.md +++ b/_doc/CHANGELOG.md @@ -2,6 +2,10 @@ 所有关于本项目的显著变更都将记录在本文件中。 +## [ To Do List ] + +- LimitSeller 逻辑优化 + ## [ In Progress ] ### 添加 diff --git a/delegate/base_subscriber.py b/delegate/base_subscriber.py index 5d22067..f1b8272 100644 --- a/delegate/base_subscriber.py +++ b/delegate/base_subscriber.py @@ -1,3 +1,4 @@ +import os import time import datetime import random @@ -289,14 +290,17 @@ def _start_scheduler(self): self.scheduler.add_job(self.daily_summary, 'cron', hour=15, minute=2) try: - print('[定时器已启动]') + print('[定时任务] 计划启动') self.scheduler.start() except KeyboardInterrupt: - print('[手动结束进程]') + print('[定时任务] 手动结束') + os.system('pause') except Exception as e: - print('策略定时器出错:', e) + print('[定时任务] 执行出错:', e) + os.system('pause') finally: self.delegate.shutdown() + print('[定时任务] 关闭完成') def create_scheduler(self): from apscheduler.schedulers.blocking import BlockingScheduler diff --git a/delegate/daily_history.py b/delegate/daily_history.py index ae3909f..7814c6d 100644 --- a/delegate/daily_history.py +++ b/delegate/daily_history.py @@ -309,7 +309,12 @@ def download_recent_daily(self, days: int) -> None: if self.data_source == DataSource.TUSHARE: now = datetime.datetime.now() all_updated_codes = set() - for forward_day in range(days, 0, -1): + # 每日 18:59 之后默认更新当日数据 + forward_end = 0 + if now.hour > 18: + forward_end -= 1 + + for forward_day in range(days, forward_end, -1): target_date = get_prev_trading_date(now, forward_day) sub_updated_codes = self._update_codes_by_tushare(target_date, code_list) all_updated_codes.update(sub_updated_codes) diff --git a/delegate/gm_callback.py b/delegate/gm_callback.py index a032b6f..ff8b597 100644 --- a/delegate/gm_callback.py +++ b/delegate/gm_callback.py @@ -45,33 +45,33 @@ def register_callback(): try: status = start(filename=file_name) if status == 0: - print(f'[掘金信息]使用{file_name}订阅回调成功') + print(f'[掘金信息] 使用 {file_name} 订阅回调成功') else: - print(f'[掘金信息]使用{file_name}订阅回调失败,状态码:{status}') + print(f'[掘金信息] 使用 {file_name} 订阅回调失败,状态码:{status}') except Exception as e0: - print(f'[掘金信息]使用{file_name}订阅回调异常:{e0}') + print(f'[掘金信息] 使用 {file_name} 订阅回调异常:{e0}') try: # 直接使用当前模块进行注册,不使用filename参数 status = start(filename='__main__') if status == 0: - print(f'[掘金信息]使用__main__订阅回调成功') + print(f'[掘金信息] 使用 __main__ 订阅回调成功') else: - print(f'[掘金信息]使用__main__订阅回调失败,状态码:{status}') + print(f'[掘金信息] 使用 __main__ 订阅回调失败,状态码:{status}') except Exception as e1: - print(f'[掘金信息]使用__main__订阅回调异常:{e1}') + print(f'[掘金信息] 使用 __main__ 订阅回调异常:{e1}') try: # 如果start()不带参数失败,尝试使用空参数 status = start() if status == 0: - print(f'[掘金信息]订阅回调成功') + print(f'[掘金信息] 订阅回调成功') else: - print(f'[掘金信息]订阅回调失败,状态码:{status}') + print(f'[掘金信息] 订阅回调失败,状态码:{status}') except Exception as e2: - print(f'[掘金信息]使用空参数订阅回调也失败:{e2}') + print(f'[掘金信息] 使用空参数订阅回调也失败:{e2}') @staticmethod def unregister_callback(): - print(f'[掘金信息]取消订阅回调') + print(f'[掘金信息] 取消订阅回调') # stop() def record_order(self, order_time: str, code: str, price: float, volume: int, side: str, remark: str): @@ -111,7 +111,7 @@ def on_execution_report(self, rpt: ExecRpt): def on_order_status(self, order: Order): if order.status == OrderStatus_Rejected: - self.ding_messager.send_text_as_md(f'订单已拒绝:{order.symbol} {order.ord_rej_reason_detail}') + self.ding_messager.send_text_as_md(f'订单驳回:{order.symbol} {order.ord_rej_reason_detail}') elif order.status == OrderStatus_Filled: stock_code = gmsymbol_to_code(order.symbol) @@ -150,26 +150,26 @@ class GmCache: def on_execution_report(rpt: ExecRpt): - # print('[掘金回调]:成交状态已变化') + # print(f'[掘金回调] {rpt.symbol} 成交状态已变化') GmCache.gm_callback.on_execution_report(rpt) def on_order_status(order: Order): - # print('[掘金回调]:订单状态已变') + # print(f'[掘金回调] {order.symbol} 订单状态已变') GmCache.gm_callback.on_order_status(order) def on_trade_data_connected(): - print('[掘金回调]交易服务已连接') + print('[掘金回调] 交易服务已连接') def on_trade_data_disconnected(): - print('[掘金回调]交易服务已断开') + print('[掘金回调] 交易服务已断开') def on_account_status(account_status: AccountStatus): - print(f'[掘金回调]账户状态已变化 状态:{account_status}') + print(f'[掘金回调] 账户状态已变化 状态:{account_status}') def on_error(error_code, error_info): - print(f'[掘金报错]错误码:{error_code} 错误信息:{error_info}') + print(f'[掘金报错] 错误码:{error_code} 错误信息:{error_info}') diff --git a/delegate/xt_subscriber.py b/delegate/xt_subscriber.py index e82e3c2..4e2a27a 100644 --- a/delegate/xt_subscriber.py +++ b/delegate/xt_subscriber.py @@ -1,3 +1,4 @@ +import os import time import datetime import json @@ -131,14 +132,14 @@ def callback_sub_whole(self, quotes: Dict) -> None: if int(curr_seconds) % self.execute_interval == 0: # 更全(默认:先记录再执行) if self.open_tick and (not self.quick_ticks): - self.record_tick_to_memory(self.cache_quotes) + self.record_tick_to_memory(self.cache_quotes) # 这里用 cache_quotes 是防止积压导致丢数据 # str(%Y-%m-%d) str(%H:%M) str(%S) dict(code: quotes) is_clear = self.execute_strategy(curr_date, curr_time, curr_seconds, self.cache_quotes) # 更快(先执行再记录) if self.open_tick and self.quick_ticks: - self.record_tick_to_memory(self.cache_quotes) + self.record_tick_to_memory(self.cache_quotes) # 这里用 cache_quotes 是防止积压导致丢数据 if is_clear: with self.lock_quotes_update: @@ -368,20 +369,21 @@ def _start_qmt_scheduler(self): # 启动定时器 try: - print('[定时进程] 任务启动') + print('[定时任务] 计划启动') self.scheduler.start() except KeyboardInterrupt: - print('[定时进程] 手动结束') + print('[定时任务] 手动结束') + os.system('pause') except Exception as e: - print('[定时进程] 任务出错:', e) + print('[定时任务] 执行出错:', e) + os.system('pause') finally: self.delegate.shutdown() - print('[定时进程] 关闭完成') + print('[定时任务] 关闭完成') try: import sys sys.exit(0) except SystemExit: - import os os._exit(0) diff --git a/run_ai_gen.py b/run_ai_gen.py index 5b65b20..edbae78 100644 --- a/run_ai_gen.py +++ b/run_ai_gen.py @@ -243,7 +243,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) - STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" + STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + '[测]' print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback diff --git a/run_redis_pull.py b/run_redis_pull.py index b4804b7..559bc6c 100644 --- a/run_redis_pull.py +++ b/run_redis_pull.py @@ -177,7 +177,7 @@ def empty_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu def redis_subscribe(): - print('[开始监听数据]') + print('[开始监听]') my_redis.subscribe(REDIS_CHANNEL) # 订阅频道 my_code_set = set(my_suber.code_list) # set 提高 in 操作的性能 O(1) 查找复杂度 for message in my_redis.listen(): @@ -198,7 +198,7 @@ def redis_subscribe(): def redis_unsubscribe(): my_redis.unsubscribe(REDIS_CHANNEL) - print('[停止监听数据]') + print('[停止监听]') def redis_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quotes: Dict) -> None: @@ -216,7 +216,7 @@ def redis_execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, cu if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) - STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" + STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + '[测]' print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback diff --git a/run_redis_push.py b/run_redis_push.py index 4be229f..28ec36d 100644 --- a/run_redis_push.py +++ b/run_redis_push.py @@ -60,7 +60,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) - STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" + STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + '[测]' print(f'[正在启动] {STRATEGY_NAME}') my_redis = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0) my_pool = Pool( diff --git a/run_remote.py b/run_remote.py index 7fe2ee8..1b24268 100644 --- a/run_remote.py +++ b/run_remote.py @@ -228,7 +228,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) - STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" + STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + '[测]' print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback diff --git a/run_shield.py b/run_shield.py index b4c6767..61b84fe 100644 --- a/run_shield.py +++ b/run_shield.py @@ -107,7 +107,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) - STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" + STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + '[测]' print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback diff --git a/run_swords.py b/run_swords.py index dcc5484..d35e8dd 100644 --- a/run_swords.py +++ b/run_swords.py @@ -203,7 +203,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) - STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" + STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + '[测]' print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback diff --git a/run_wencai_qmt.py b/run_wencai_qmt.py index 4e00ba5..23bc4e3 100644 --- a/run_wencai_qmt.py +++ b/run_wencai_qmt.py @@ -197,7 +197,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) - STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" + STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + '[测]' print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback diff --git a/run_wencai_tdx.py b/run_wencai_tdx.py index 30e34fe..0f6a8c6 100644 --- a/run_wencai_tdx.py +++ b/run_wencai_tdx.py @@ -195,7 +195,7 @@ def execute_strategy(curr_date: str, curr_time: str, curr_seconds: str, curr_quo if __name__ == '__main__': logging_init(path=PATH_LOGS, level=logging.INFO) - STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + "[测]" + STRATEGY_NAME = STRATEGY_NAME if IS_PROD else STRATEGY_NAME + '[测]' print(f'[正在启动] {STRATEGY_NAME}') if IS_PROD: from delegate.xt_callback import XtCustomCallback diff --git a/tools/utils_basic.py b/tools/utils_basic.py index a4cebc2..b8d5a76 100644 --- a/tools/utils_basic.py +++ b/tools/utils_basic.py @@ -187,20 +187,27 @@ def gmsymbol_to_code(gmsymbol: str) -> str: # 判断是不是可交易股票代码 包含 股票 ETF 可转债 def is_symbol(code_or_symbol: str): return code_or_symbol[:2] in [ - '00', '30', # 深交所 - '60', '68', # 上交所 - '82', '83', '87', '88', '43', '92', # 北交所 - '15', '51', '52', '53', '55', '56', '58', # ETF - '11', '12', # 可转债 + '00', '30', # 深交所 + '60', '68', # 上交所 + '82', '83', '87', '88', '43', '92', # 北交所 + '15', '51', '52', '53', '55', '56', '58', # ETF + '11', '12', # 可转债 ] def is_stock(code_or_symbol: str | int): - """ 判断是不是股票代码 """ code_or_symbol = str(code_or_symbol) if type(code_or_symbol) == int else code_or_symbol return code_or_symbol[:2] in ['00', '30', '60', '68', '82', '83', '87', '88', '43', '92'] +def is_stock_code(code: str): + """ 判断是不是股票代码 """ + ex = code.split('.')[1] + if ex == 'SH' and code[:2] in ['00']: + return False + return code[:2] in ['00', '30', '60', '68', '82', '83', '87', '88', '43', '92'] + + def is_stock_10cm(code_or_symbol: str | int): """ 判断是不是10cm票 """ code_or_symbol = str(code_or_symbol) if type(code_or_symbol) == int else code_or_symbol diff --git a/tools/utils_cache.py b/tools/utils_cache.py index 763cb63..2eb0844 100644 --- a/tools/utils_cache.py +++ b/tools/utils_cache.py @@ -557,7 +557,7 @@ def check_is_open_day_sina(curr_date: str) -> bool: # 网络缓存 df = ak.tool_trade_date_hist_sina() df.to_csv(TRADE_DAY_CACHE_PATH) - print(f'Cache trade day list {curr_year} - {int(curr_year) + 1} in {TRADE_DAY_CACHE_PATH}.') + print(f'[网络缓存] 更新交易日历 {curr_year} - {int(curr_year) + 1} 已存入 {TRADE_DAY_CACHE_PATH}.') trade_day_list = get_disk_trade_day_list_and_update_max_year() if curr_year <= trade_day_cache[trade_max_year_key]: # 未过期 From 225bc343a5529e8e3d284a37a60f8e1e5597bb63 Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:10:45 +0800 Subject: [PATCH 15/16] =?UTF-8?q?=E5=8F=98=E9=87=8F=E5=90=8D=E7=BB=9F?= =?UTF-8?q?=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/seller_components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trader/seller_components.py b/trader/seller_components.py index fe0ea30..3980d4e 100644 --- a/trader/seller_components.py +++ b/trader/seller_components.py @@ -49,7 +49,7 @@ class SafeSeller(BaseSeller): def __init__(self, strategy_name, delegate, parameters): BaseSeller.__init__(self, strategy_name, delegate, parameters) print('临跌停卖点模块', end=' ') - self.stop_time_range = parameters.hard_time_range + self.safe_time_range = parameters.hard_time_range self.safe_rate = parameters.safe_rate def check_sell( @@ -57,7 +57,7 @@ def check_sell( position: XtPosition, held_day: int, max_price: Optional[float], history: Optional[pd.DataFrame], ticks: Optional[list[list]], extra: any, ) -> bool: - if (held_day > 0) and (self.stop_time_range[0] <= curr_time < self.stop_time_range[1]): + if (held_day > 0) and (self.safe_time_range[0] <= curr_time < self.safe_time_range[1]): curr_price = quote['lastPrice'] last_close = quote['lastClose'] sell_volume = position.can_use_volume From 52b5b3c8268a1b54959351ee612f205c620dac0c Mon Sep 17 00:00:00 2001 From: silver6wings <3032247+silver6wings@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:11:03 +0800 Subject: [PATCH 16/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=A7=91=E5=88=9B?= =?UTF-8?q?=E6=9C=80=E5=B0=91=E4=B9=B0200=E8=82=A1=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/buyer.py | 81 +++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/trader/buyer.py b/trader/buyer.py index 5ce2e44..1ca0e4b 100644 --- a/trader/buyer.py +++ b/trader/buyer.py @@ -4,7 +4,7 @@ from delegate.base_delegate import BaseDelegate -from tools.utils_basic import get_limit_up_price, debug +from tools.utils_basic import get_limit_up_price, debug, is_stock_kc DEFAULT_BUY_REMARK = '买入委托' @@ -111,55 +111,62 @@ def order_buy( remark: str, market: bool = True, log: bool = True, - ): + ) -> bool: buy_volume = volume if self.risk_control and buy_volume > self.slot_capacity / price: buy_volume = math.floor(self.slot_capacity / price / 100) * 100 logging.warning(f'{code} 超过风险控制,买入量调整为 {buy_volume} 股') - if buy_volume > 0: - order_price = price + self.order_premium - limit_price = get_limit_up_price(code, last_close) - - if market: - buy_type = '市买' - if order_price > limit_price: - # 如果涨停了只能挂限价单 - self.delegate.order_limit_open( - code=code, - price=limit_price, - volume=buy_volume, - remark=remark, - strategy_name=self.strategy_name) - else: - self.delegate.order_market_open( - code=code, - price=min(order_price, limit_price), - volume=buy_volume, - remark=remark, - strategy_name=self.strategy_name) - else: - buy_type = '限买' + if buy_volume < 1: + print(f'{code} 挂单买量为0,不委托') + return False + + if buy_volume < 200 and is_stock_kc(code): + print(f'{code} 科创最少200,不委托') + return False + + order_price = price + self.order_premium + limit_price = get_limit_up_price(code, last_close) + + if market: + buy_type = '市买' + if order_price > limit_price: + # 如果涨停了只能挂限价单 self.delegate.order_limit_open( code=code, - price=min(order_price, limit_price), + price=limit_price, volume=buy_volume, remark=remark, strategy_name=self.strategy_name) - - if log: - logging.warning(f'{buy_type}委托 {code} \t现价:{price:.3f} {buy_volume}股') - - if self.delegate.callback is not None: - self.delegate.callback.record_order( - order_time=datetime.datetime.now().timestamp(), + else: + self.delegate.order_market_open( code=code, - price=price, + price=min(order_price, limit_price), volume=buy_volume, - side=f'{buy_type}委托', - remark=remark) + remark=remark, + strategy_name=self.strategy_name) else: - print(f'{code} 挂单买量为0,不委托') + buy_type = '限买' + self.delegate.order_limit_open( + code=code, + price=min(order_price, limit_price), + volume=buy_volume, + remark=remark, + strategy_name=self.strategy_name) + + if log: + logging.warning(f'{buy_type}委托 {code} \t现价:{price:.3f} {buy_volume}股') + + if self.delegate.callback is not None: + self.delegate.callback.record_order( + order_time=datetime.datetime.now().timestamp(), + code=code, + price=price, + volume=buy_volume, + side=f'{buy_type}委托', + remark=remark) + + return True class LimitedBuyer(BaseBuyer):