fix bug
This commit is contained in:
29
docs/bug.md
Normal file
29
docs/bug.md
Normal file
@@ -0,0 +1,29 @@
|
||||
### S1. 券商快照延迟时,本地委托锁会被清空并可能重复下单
|
||||
|
||||
- 位置:`strategy/trend/order.py:54-92`、`strategy/trend/boot.py:127-130,157-162`、`strategy/trend/open.py:23-25`
|
||||
- 证据:下单成功后 `OrderBook.place()` 会立即加入本地 pending 和方向锁,但下一轮 `refresh()` 会完全用券商快照重建 `data` 与 `lock`。若券商快照尚未出现刚提交的订单,本地 pending 会直接丢失。开仓候选只排除当前持仓,不排除 `State` 中的待成交底仓,最终仅依赖已经被清掉的 `busy()` 锁。
|
||||
- 影响:接口存在可见性延迟时,同一证券可能在连续轮次重复提交买单;卖单和补仓也存在相同锁丢失窗口。
|
||||
- 建议:刷新时合并尚未超时且券商未确认的本地 pending,而不是覆盖;按本地订单号查询确认后才能移除。开仓筛选同时检查状态机中的 `base_status`。
|
||||
|
||||
|
||||
### H3. 撤单、拒单、废单和部分成交不能驱动状态机正确收敛
|
||||
|
||||
- 位置:`strategy/trend/state.py:142-162`、`strategy/trend/order.py:14-17,61-92`
|
||||
- 证据:状态对账只把“匹配订单全部为状态 56”视为完成,其余均保持 `ING`;订单被过滤或消失时,已有持仓的补仓状态不会变为失败或撤销。文件中虽然定义了 `FAILED`、`CANCELED`、`UNKNOWN`,但没有完整迁移逻辑。
|
||||
- 影响:失败的补仓仍会消耗 `added_num`,状态可能永久停留在处理中,重启后也无法可靠恢复。
|
||||
- 建议:建立完整 QMT 委托状态映射,按实际成交数量处理全成、部成、已撤、废单、拒单和未知;消失订单需二次查询确认。
|
||||
|
||||
|
||||
### M1. IPO 仍只依赖本地锁,存在重复申购窗口
|
||||
|
||||
- 位置:`strategy/ipo/boot.py:49-65`
|
||||
- 证据:下单成功后才写锁;未查询券商当日委托或成交记录。进程若在下单成功与写锁之间退出,或锁文件被删除,下一次调度会再次申购。
|
||||
- 影响:同一证券可能重复发送申购请求,安全性依赖券商端是否拒绝重复申购。
|
||||
- 建议:本地锁只作为快速缓存,下单前以券商委托/成交记录做最终幂等校验。
|
||||
|
||||
### M6. 策略成本使用提交时行情价,而非实际成交价
|
||||
|
||||
- 位置:`strategy/trend/open.py:80-86`、`strategy/trend/positions.py:186-192`
|
||||
- 证据:底仓和补仓在订单刚提交成功时就把 tick 价格写为成本,没有根据成交回报更新实际成交数量与均价。
|
||||
- 影响:滑点、部分成交或拆单时,后续盈亏率、止盈网格和补仓层级基于不准确成本。
|
||||
- 建议:提交阶段只记录订单标识;订单完成对账后从成交或真实持仓均价更新成本和数量。
|
||||
@@ -2,59 +2,69 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import config
|
||||
from sdk import Client
|
||||
from libs.calc import trading_time
|
||||
from libs.lockfile import is_lock,write_lockfile
|
||||
|
||||
IPO_STRATEGY_NAME = "IPO_SUBSCRIBE"
|
||||
IPO_REMARKS = {IPO_STRATEGY_NAME, "新股申购"}
|
||||
IPO_SESSIONS = ((time(9, 30), time(11, 30)), (time(13, 0), time(15, 0)))
|
||||
TRADING_CALENDAR_SYMBOL = "000001.SH"
|
||||
|
||||
|
||||
def AutoBuyIpo() -> int:
|
||||
def AutoBuyIpo() -> None:
|
||||
"""安全执行一次新股申购,返回成功提交的证券数量。"""
|
||||
if not config.account_config.enable_auto_ipo:
|
||||
logging.info("[IPO] 自动申购未启用")
|
||||
return 0
|
||||
return
|
||||
if not trading_time(datetime.now()):
|
||||
logging.info("[IPO] 非交易时间")
|
||||
return 0
|
||||
return
|
||||
|
||||
client = Client(
|
||||
config.global_config.qmt_base_url,
|
||||
config.global_config.qmt_token,
|
||||
config.HTTP_TIMEOUT,
|
||||
)
|
||||
try:
|
||||
with Client(
|
||||
config.global_config.qmt_base_url,
|
||||
config.global_config.qmt_token,
|
||||
config.HTTP_TIMEOUT,
|
||||
) as client:
|
||||
result = client.ipo_data("STOCK")
|
||||
for item in result:
|
||||
try:
|
||||
if not isinstance(item, dict):
|
||||
raise TypeError("IPO 数据项必须是字典")
|
||||
|
||||
stock = str(item.get("stock", "")).strip()
|
||||
if not stock or stock.endswith(".BJ"):
|
||||
continue
|
||||
|
||||
ipo_price = float(item["issuePrice"])
|
||||
max_purchase_num = int(item["maxPurchaseNum"])
|
||||
if ipo_price <= 0 or max_purchase_num <= 0:
|
||||
raise ValueError("发行价或申购额度必须大于 0")
|
||||
|
||||
lock_path = Path(config.global_config.qmt_data_dir) / f"{stock}.lock"
|
||||
if is_lock(lock_path):
|
||||
continue
|
||||
|
||||
client.passorder(
|
||||
op_type=23,
|
||||
stock=stock,
|
||||
volume=max_purchase_num,
|
||||
pr_type=11,
|
||||
price=ipo_price,
|
||||
strategy_name="ipo",
|
||||
)
|
||||
write_lockfile(lock_path)
|
||||
logging.info(
|
||||
"[IPO] %s 申购,发行价:%s 可申购额度:%s",
|
||||
stock,
|
||||
ipo_price,
|
||||
max_purchase_num,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("[IPO] 单条申购处理失败,数据=%r", item)
|
||||
except Exception:
|
||||
logging.exception("[IPO] 自动申购任务失败")
|
||||
|
||||
result = client.ipo_data("STOCK")
|
||||
for stock in result:
|
||||
if ".BJ" in stock:
|
||||
continue
|
||||
lp = Path(config.global_config.qmt_data_dir)/f"{stock}.lock"
|
||||
if not is_lock(lp):
|
||||
ipo_price = result[stock]['issuePrice'] # 发行价
|
||||
maxPurchaseNum = result[stock]['maxPurchaseNum'] # 可申购额度
|
||||
try:
|
||||
client.passorder(
|
||||
op_type=23,
|
||||
stock=stock,
|
||||
volume=maxPurchaseNum,
|
||||
pr_type=11,
|
||||
price=ipo_price,
|
||||
strategy_name="新股申购",
|
||||
)
|
||||
logging.info("[IPO] %s 申购,发行价:%s 可申购额度:%s", stock)
|
||||
except Exception:
|
||||
logging.info("[IPO] %s 申购失败,不写入锁文件", stock)
|
||||
else:
|
||||
write_lockfile(lp)
|
||||
|
||||
client.close()
|
||||
|
||||
@@ -89,6 +89,8 @@ def StartTrend() -> None:
|
||||
lt = time.localtime()
|
||||
if (lt.tm_hour, lt.tm_min, lt.tm_sec) >= (15, 0, 0):
|
||||
log.info("[Trend] 已到 15:00,结束趋势策略")
|
||||
run.client.close()
|
||||
run.executor.shutdown()
|
||||
return
|
||||
current_sec = lt.tm_sec
|
||||
|
||||
|
||||
@@ -17,8 +17,20 @@ def open_signal(run:Runtime, ticks, open_signals) -> None:
|
||||
for item in open_signals:
|
||||
# 1. 验证信号配置允许开仓的时间区间。
|
||||
signal_config = run.global_cfg.signals.get(item.signal_key)
|
||||
if signal_config is None or not check_timezone(signal_config.timezone):
|
||||
log.info("[Open] %s 信号=%s,跳过:不在信号时间段(%s)", item.code, item.signal_key,signal_config.timezone)
|
||||
if signal_config is None:
|
||||
log.warning(
|
||||
"[Open] %s 信号=%s,跳过:未找到信号配置",
|
||||
item.code,
|
||||
item.signal_key,
|
||||
)
|
||||
continue
|
||||
if not check_timezone(signal_config.timezone):
|
||||
log.info(
|
||||
"[Open] %s 信号=%s,跳过:不在信号时间段(%s)",
|
||||
item.code,
|
||||
item.signal_key,
|
||||
signal_config.timezone,
|
||||
)
|
||||
continue
|
||||
|
||||
# 2. 检查该证券是否已有买入委托锁,防止重复下单。
|
||||
|
||||
Reference in New Issue
Block a user