Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions _doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

- LimitSeller 逻辑优化

## [ 4.5.0 ] 2026-02-25

### 修改
- 修复最小价格记录的bug
- 稳定性提高,防御部分网络抖动
- 控制台输出文案优化

## [ 4.4.0 ] 2026-01-29

### 修改
Expand Down
33 changes: 18 additions & 15 deletions delegate/base_delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def order_cancel_sell(self, code: str, strategy_name: str = DEFAULT_STRATEGY_NAM
pass

@staticmethod
def is_position_holding(position: any) -> bool:
def is_position_holding(position: dict) -> bool:
return False

@abstractmethod
Expand All @@ -93,28 +93,31 @@ def shutdown(self) -> None:


# -----------------------
# 持仓自动发现
# 持仓自动发现更新进缓存
# -----------------------
def update_position_held(lock: threading.Lock, delegate: BaseDelegate, path: str):
with lock:
positions = delegate.check_positions()
held_info = load_json(path)
if positions is None:
print('[扫描仓位] 未能获取持仓数据')
elif len(positions) <= 0:
print('[扫描仓位] 当前空仓')
else:
position_codes = [position.stock_code for position in positions]
print('[扫描仓位] 当前持仓', position_codes)

# 添加未被缓存记录的持仓:默认当日买入
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 = load_json(path)

# 删除已清仓的held_info记录
if positions is not None and len(positions) > 0:
position_codes = [position.stock_code for position in positions]
print('[当前持仓]', position_codes)
# 添加未被缓存记录的持仓:默认当日买入
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记录
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)
save_json(path, held_info)
94 changes: 64 additions & 30 deletions delegate/base_subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
from tools.utils_remote import DataSource, ExitRight, get_daily_history


def get_today() -> str:
return datetime.datetime.today().strftime('%Y-%m-%d')


class BaseSubscriber:
def __init__(
self,
Expand Down Expand Up @@ -96,13 +100,14 @@ def __init__(
self.history_day_klines : Dict[str, pd.DataFrame] = {}

self.code_list = []
self.curr_trade_date = ''
self.before_job_success_flag = ''
self.near_trade_success_flag = ''

# -----------------------
# 监测主策略执行
# -----------------------
def callback_run_no_quotes(self):
if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')):
if not check_is_open_day(get_today()):
return

now = datetime.datetime.now()
Expand All @@ -113,7 +118,7 @@ def callback_run_no_quotes(self):
# 每分钟输出一行开头
if self.cache_limits['prev_minutes'] != curr_time:
self.cache_limits['prev_minutes'] = curr_time
print(f'\n[{curr_time}]', end='')
print(f'\n[{curr_date} {curr_time}]', end='')

curr_seconds = now.strftime('%S')
if self.cache_limits['prev_seconds'] != curr_seconds:
Expand All @@ -125,7 +130,7 @@ def callback_run_no_quotes(self):
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')):
if not check_is_open_day(get_today()):
return

if self.messager is not None:
Expand All @@ -137,7 +142,7 @@ def callback_open_no_quotes(self):
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')):
if not check_is_open_day(get_today()):
return

print('\n[关闭策略]')
Expand All @@ -149,55 +154,81 @@ def callback_close_no_quotes(self):
threading.Thread(target=self.custom_end_unsub).start()

def update_code_list(self, code_list: list[str]):
print(f'[订阅列表]:{code_list}\n', end='')
self.code_list = code_list

# -----------------------
# 任务接口
# -----------------------
def clear_all(self):
self.history_day_klines.clear()

def before_trade_day_wrapper(self):
if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')):
if not check_is_open_day(get_today()):
return

self.clear_all()

if self.before_trade_day is not None:
self.before_trade_day()
self.curr_trade_date = datetime.datetime.now().strftime('%Y-%m-%d')
print(f'[定时任务] 盘前任务开始')
try:
self.before_trade_day()
self.before_job_success_flag = get_today()
print(f'[定时任务] 盘前任务完成 {self.before_job_success_flag}\n', end='')
except Exception as e:
print(f'[定时任务] 盘前任务出错: {e}\n', end='')

def near_trade_begin_wrapper(self):
if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')):
if not check_is_open_day(get_today()):
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'今日盘前准备工作已完成')
print(f'[定时任务] 临盘任务开始')
try:
self.near_trade_begin()
self.near_trade_success_flag = get_today()
print(f'[定时任务] 临盘任务完成 {self.near_trade_success_flag}\n', end='')
except Exception as e:
print(f'[定时任务] 临盘任务出错: {e}\n', end='')


def finish_trade_day_wrapper(self):
if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')):
if not check_is_open_day(get_today()):
return

if self.finish_trade_day is not None:
self.finish_trade_day()
print(f'[定时任务] 盘后任务开始')
try:
self.finish_trade_day()
print(f'[定时任务] 盘后任务完成\n', end='')
except Exception as e:
print(f'[定时任务] 盘后任务出错: {e}\n', end='')

def execute_call_end_wrapper(self):
if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')):
if not check_is_open_day(get_today()):
return

print('[竞价结束回调]')
if self.execute_call_end is not None:
print(f'[定时任务] 竞价任务开始')
self.execute_call_end()
try:
print(f'[定时任务] 竞价任务完成\n', end='')
except Exception as e:
print(f'[定时任务] 竞价任务出错: {e}\n', end='')

# 检查是否完成盘前准备
def check_before_finished(self):
if not check_is_open_day(datetime.datetime.now().strftime('%Y-%m-%d')):
if not check_is_open_day(get_today()):
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]盘前准备未完成,尝试重新执行盘前函数')
if self.before_trade_day is not None and self.before_job_success_flag != get_today():
print('[定时任务] 重新执行盘前任务开始\n', end='')
self.before_trade_day_wrapper()
print(f'[定时任务] 重新执行盘前任务完成\n', end='')
if self.near_trade_begin is not None and self.near_trade_success_flag != get_today():
print(f'[定时任务] 重新执行临盘任务开始\n', end='')
self.near_trade_begin_wrapper()
print(f'当前交易日:[{self.curr_trade_date}]')
print(f'[定时任务] 重新执行临盘任务完成\n', end='')

# -----------------------
# 盘后报告总结
Expand All @@ -207,13 +238,14 @@ def daily_summary(self):
if not check_is_open_day(now.strftime('%Y-%m-%d')):
return

print(f'[每日总结] 开始')
curr_date = 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)
print('[每日总结] 交易报告出错: ', e)
traceback.print_exc()

if self.open_today_hold_report:
Expand All @@ -222,19 +254,21 @@ def daily_summary(self):
positions = self.delegate.check_positions()
self.daily_reporter.today_hold_report(today=curr_date, positions=positions)
else:
print('Missing delegate to complete reporting!')
print('[每日总结] 获取持仓信息必需 delegate')
except Exception as e:
print('Report position failed: ', e)
print('[每日总结] 持仓报告出错: ', 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, is_afternoon=(now.hour > 12))
except Exception as e:
print('Report asset failed: ', e)
print('[每日总结] 账户报告出错: ', e)
traceback.print_exc()

print(f'[每日总结] 结束')

# -----------------------
# 定时器
# -----------------------
Expand Down Expand Up @@ -336,7 +370,7 @@ 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='')
print(f'\n[{curr_date} {curr_time}]', end='')
is_open_day = check_is_open_day(curr_date)
if self.delegate is not None:
self.delegate.is_open_day = is_open_day
Expand Down Expand Up @@ -384,7 +418,7 @@ def _download_from_remote(
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]):
def _download_from_tdx(self, target_codes: list, start: str, end: str, adjust: ExitRight, columns: list[str]):
print(f'Prepared time range: {start} - {end}')
t0 = datetime.datetime.now()

Expand Down Expand Up @@ -444,7 +478,7 @@ def download_cache_history(
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:
if data_source == DataSource.TDXZIP and (self.history_day_klines is None or len(self.history_day_klines) == 0):
self.history_day_klines = get_tdxzip_history(adjust=adjust)

# ======== 预加载每日增量数据源 ========
Expand All @@ -466,7 +500,7 @@ def download_cache_history(

# 重新加载历史数据进内存
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')):
if not check_is_open_day(get_today()):
return

hc = DailyHistoryCache()
Expand Down
30 changes: 15 additions & 15 deletions delegate/gm_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,33 @@ def register_callback():
try:
status = start(filename=file_name)
if status == 0:
print(f'[掘金信息] 使用 {file_name} 订阅回调成功')
print(f'[掘金信息] 使用 {file_name} 订阅回调成功\n', end='')
else:
print(f'[掘金信息] 使用 {file_name} 订阅回调失败,状态码:{status}')
print(f'[掘金信息] 使用 {file_name} 订阅回调失败,状态码:{status}\n', end='')
except Exception as e0:
print(f'[掘金信息] 使用 {file_name} 订阅回调异常:{e0}')
print(f'[掘金信息] 使用 {file_name} 订阅回调异常:{e0}\n', end='')
try:
# 直接使用当前模块进行注册,不使用filename参数
status = start(filename='__main__')
if status == 0:
print(f'[掘金信息] 使用 __main__ 订阅回调成功')
print(f'[掘金信息] 使用 __main__ 订阅回调成功\n', end='')
else:
print(f'[掘金信息] 使用 __main__ 订阅回调失败,状态码:{status}')
print(f'[掘金信息] 使用 __main__ 订阅回调失败,状态码:{status}\n', end='')
except Exception as e1:
print(f'[掘金信息] 使用 __main__ 订阅回调异常:{e1}')
print(f'[掘金信息] 使用 __main__ 订阅回调异常:{e1}\n', end='')
try:
# 如果start()不带参数失败,尝试使用空参数
status = start()
if status == 0:
print(f'[掘金信息] 订阅回调成功')
print(f'[掘金信息] 订阅回调成功\n', end='')
else:
print(f'[掘金信息] 订阅回调失败,状态码:{status}')
print(f'[掘金信息] 订阅回调失败,状态码:{status}\n', end='')
except Exception as e2:
print(f'[掘金信息] 使用空参数订阅回调也失败:{e2}')
print(f'[掘金信息] 使用空参数订阅回调也失败:{e2}\n', end='')

@staticmethod
def unregister_callback():
print(f'[掘金信息] 取消订阅回调')
print(f'[掘金信息] 取消订阅回调\n', end='')
# stop()

def record_order(self, order_time: str, code: str, price: float, volume: int, side: str, remark: str):
Expand Down Expand Up @@ -142,7 +142,7 @@ def on_order_status(self, order: Order):
elif order.status == OrderStatus_Canceled:
print(f'[CANCELED:{order.symbol}]', end='')
else:
print(f'[掘金订单]{order.symbol} 订单状态:{order.status}')
print(f'[掘金订单]{order.symbol} 订单状态:{order.status}\n', end='')


class GmCache:
Expand All @@ -160,16 +160,16 @@ def on_order_status(order: Order):


def on_trade_data_connected():
print('[掘金回调] 交易服务已连接')
print('[掘金回调] 交易服务已连接\n', end='')


def on_trade_data_disconnected():
print('[掘金回调] 交易服务已断开')
print('[掘金回调] 交易服务已断开\n', end='')


def on_account_status(account_status: AccountStatus):
print(f'[掘金回调] 账户状态已变化 状态:{account_status}')
print(f'[掘金回调] 账户状态已变化 状态:{account_status}\n', end='')


def on_error(error_code, error_info):
print(f'[掘金报错] 错误码:{error_code} 错误信息:{error_info}')
print(f'[掘金报错] 错误码:{error_code} 错误信息:{error_info}\n', end='')
Loading
Loading