处理命令冲突的问题
This commit is contained in:
parent
be92b2be8a
commit
9a1b93a6d2
34
DEVLOG.md
34
DEVLOG.md
|
|
@ -30,6 +30,40 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-08-20(续)· 清仓与减仓对策略的优先级理顺:减仓不掐网格、清仓补撤在途买单、在途文案分开
|
||||
|
||||
**背景**
|
||||
上一节紧急清仓修复留了三笔尾账,这次结掉前两笔。同时按用户要求排查一件更要紧的事:有没有哪里会让网格建仓、做T这类个股策略被误挡失效。用户给了判定尺子——清仓是优先级最高的,可以覆盖一切;减仓则要看情况,默认不该动策略。全部改动都在持仓系统的执行层与命令层,其他系统不动。
|
||||
|
||||
**做了什么(三处,都带单测)**
|
||||
一,减仓命令不再撤掉策略腿的在途买单。原来整体降仓命令会把所有在途买入指令一律撤掉,其中就包括网格和做T挂出的买单,等于一执行减仓、网格就被掐断。现在减仓只撤非策略的在途买入(自主、提议、命令三种照撤,因为减仓时确实不该再往里投新钱),策略腿留着继续跑。清仓类命令仍然全撤、一股不留,因为清仓优先级最高。补一个理由:减仓命令只物化一次,就算撤了策略的在途买单,策略下一分钟又照发一张,撤了等于没撤,只是白撤一笔、还在页面上报“撤了几条策略买单”误导人。
|
||||
|
||||
二,清仓某股和清仓某行业补上撤在途买单这一层。上一节给三类清仓命令都加了即时撤策略,但只有一键清仓的方案生成器会顺带把在途买单排进撤单项。清仓某股和清仓某行业的方案生成器只排一条卖出,不排撤买单。结果是撤策略只改了策略状态,已经下发到券商的那张网格或做T买单还挂着继续成交——你正清仓这只票,它却又买进来一笔当天卖不掉的货。现在即时停买入侧这一步里,对被清仓的票直接撤掉当前在途买入指令。一键清仓时这些单多半已被撤过、不在在途集里,补这道只是空跑,幂等无害。
|
||||
|
||||
三,把“已全部在途、等成交”和“当日配额已出完”这两句话分开。上一节修好配额口径之后,紧急清仓和窗口末日单在“该出的量都挂在券商正等成交”这种情形下,仍复用“当日配额已出完”这句话,行为对但措辞会让人误以为又犯了老毛病。现在紧急单和末日单在可再投放为零时,改报“已全部在途,等成交回来”;普通多日单才报“当日配额已出完”。两条择时路径(内置择时的硬闸、策略即时决策)各改一处,用同一个函数出这句话,口径不串。
|
||||
|
||||
**排查结论:网格建仓与做T有没有被误挡**
|
||||
通读了策略层和规则闸,结论是没有系统性失效,但有两处值得拍板的地方,这次先不擅自改,列出来等你定。
|
||||
先说没问题的三点。昨夜决策系统看空某只票,不会挡住这只票的网格买入,因为策略即时那条路本就不带看空定性,网格照常逐档买。宏观偏热闸和买入暂停都只挡买入开腿,卖出、平回、跟踪止盈一律照常,符合设计。做T的平回腿在熔断和买入暂停下都放行,不会把一条开着的T仓卡住。
|
||||
待定第一处:组合刹车(熔断)触发时,网格的买入腿会被规则闸挡下。原因是网格买单在系统里标为“非命令”,规则闸对非命令的自主增持在刹车期一律拦,对命令只提示不拦。可网格恰恰是逢跌买入,跌得狠正是刹车最容易触发的时候,于是刹车一响、网格买入全停。做T不受这条影响,因为做T标为命令、刹车只对它提示。问题是网格算“自主”还是算“你特意下的命令”。若你认为网格也该像做T那样在刹车期只提示不拦,我再改;这是安全阀的行为改动,不敢擅动。
|
||||
待定第二处:全局暂停买入(你手动踩的买入总闸)打开时,做T的买回腿也会被规则闸挡下。反T是先卖后买、要靠买回来平掉,买回被挡就意味着这一轮T只卖不买、把底仓卖出去没接回来,连14:50强制平回也会被同一道闸挡住。这多半是你的本意——既然全局停买,就连做T买回也别买;但副作用是反T会留下没接回的敞口。要不要给做T的平回腿在全局暂停买入下开个口子,请你定。
|
||||
|
||||
**拍板(2026-08-20)**:上面两处都保持现状不改。刹车(熔断)照挡网格买入腿——刹车是组合级熔断,网格逢跌买入正撞在最该拦的时候,先不放开更稳。全局暂停买入照挡做T买回腿——买入总闸的语义要一致,代价是反T会留一小截没接回的敞口,属已知取舍。两处都无需改码,当前行为即最终行为。
|
||||
|
||||
**动了哪些文件**
|
||||
app/core/exec_timing.py(新增 quota_wait_reason 出“在途/配额已出完”两句话,硬闸 left<=0 分支改用它);app/services/exec_advisor.py(策略即时决策同样按在途口径出话,签名加 is_last_day 与 urgent 透传);app/services/command_service.py(减仓走 _pending_buys(exclude_strategy=True) 剔策略腿;_stop_buyside_for_exit 补撤在途买单一层,进度回执多一项 instructions);scripts/test_batch15_units.py(原 7 例扩到 12 例:新增文案拆分四例、减仓剔策略腿一例,原撤策略例加断言撤在途买单);scripts/run_tests.py(batch15 计数改 12,总数 510)。
|
||||
|
||||
**部署方式**
|
||||
桥机 factorevaluation 上 make deploy(源码打进镜像,必须重建容器),make test 见 ALL SUITES PASS(已含改后 batch15)。收盘后部署,让下一次清仓或减仓发生在你在场时。不新增参数,不动 .env。
|
||||
|
||||
**真机判收**
|
||||
未判收。开发容器全量单测 ALL SUITES PASS(510 例,batch15 由 7 例扩到 12 例)。判收建议:下达减仓命令后,挂着策略的票在命令进度回执 stopped_buyside 里不再报撤策略买单,网格继续按档买卖;对某只挂策略的票下清仓某股,命令进度回执 stopped_buyside.instructions 里能看到那张在途买单被撤;盯盘时“已全部在途、等成交回来”与“当日配额已出完”两句话分得开。
|
||||
|
||||
**还欠着什么**
|
||||
一,上面两处待定(刹车挡网格买、全局暂停买入挡做T买回)已于当日拍板:都保持现状不改,无需改码,详见上面“拍板”一段。二,减仓是否要更进一步、连策略本身也暂停(而不只是不撤在途买单),本次按“默认不动策略”处理;若你想要“减仓也暂停这些票的策略买入开腿”这种中间档,说一声再加。三,上一节欠的第三笔——持仓系统侧可卖量与券商侧可用持仓偶有不一致(出口委托里有 INSUFFICIENT_POSITION 拒单),仍未动,属对账口径,单独排查。
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-20 · 紧急清仓修复:配额不重复扣今日成交 / 清仓命令即时撤策略
|
||||
|
||||
**背景**
|
||||
|
|
|
|||
|
|
@ -150,6 +150,19 @@ def _bucket_due(*, now_min: int, quota: int, fired: int, params: dict, eod_min:
|
|||
f"累计应出 {due} 股、实出 {int(fired or 0)} 股, 补齐 {qty} 股"}
|
||||
|
||||
|
||||
def quota_wait_reason(urgent: bool = False, is_last_day: bool = False) -> str:
|
||||
"""left<=0 (可再投放为零) 这一句该怎么说 —— 同一个数, 两种完全相反的情形:
|
||||
|
||||
* 紧急单 / 窗口末日单: 「今日已投放量」按**在途量**算 (run_tick 走 _inflight_today),
|
||||
于是 left=0 的含义是「该出的量全挂在券商、正等成交回来」, 并**不是**今天不再出手。
|
||||
* 普通多日单: 「今日已投放量」按今日成交+在途算 (_consumed_today), left=0 才是真的
|
||||
「当日节流到顶、今天不再补」。
|
||||
|
||||
行为两者一样 (都 ACT_WAIT), 但话必须分开 —— 2026-08-03/08-19 两次盘中都是盯着
|
||||
「当日配额已出完」这一句反复排查, 真相却是单子正挂着等成交。措辞分开, 一眼看得出下场。"""
|
||||
return "已全部在途, 等成交回来" if (urgent or is_last_day) else "当日配额已出完"
|
||||
|
||||
|
||||
def hard_gate(*, side: str, now, day: dict, params: dict, is_last_day: bool,
|
||||
is_command: bool, fired_today: int = 0, quota: int = 0,
|
||||
urgent: bool = False):
|
||||
|
|
@ -182,7 +195,7 @@ def hard_gate(*, side: str, now, day: dict, params: dict, is_last_day: bool,
|
|||
if day.get("halted"):
|
||||
return out(ACT_SKIP, "停牌, 当日跳过顺延")
|
||||
if left <= 0:
|
||||
return out(ACT_WAIT, "当日配额已出完")
|
||||
return out(ACT_WAIT, quota_wait_reason(urgent, is_last_day))
|
||||
if not in_session(now_min):
|
||||
return out(ACT_WAIT, "非交易时段")
|
||||
|
||||
|
|
|
|||
|
|
@ -271,9 +271,10 @@ def plan_command(cmd: dict) -> dict:
|
|||
if cancel_failed:
|
||||
notes.insert(0, f"⚠ {len(cancel_failed)} 条在途指令**没撤掉, 仍在下游挂着**: "
|
||||
+ "; ".join(f"{x['instruction_id']}({x['why']})" for x in cancel_failed[:5]))
|
||||
if stop_r.get("strategies") or stop_r.get("proposals"):
|
||||
if stop_r.get("strategies") or stop_r.get("proposals") or stop_r.get("instructions"):
|
||||
notes.insert(0, f"清仓覆盖冲突: 已撤策略 {len(stop_r['strategies'])} 条、"
|
||||
f"驳回买入提议 {len(stop_r['proposals'])} 条 (停止边卖边买)")
|
||||
f"驳回买入提议 {len(stop_r['proposals'])} 条、"
|
||||
f"撤在途买单 {len(stop_r.get('instructions') or [])} 条 (停止边卖边买)")
|
||||
if stop_r.get("errors"):
|
||||
notes.insert(0, f"⚠ 停买入侧有 {len(stop_r['errors'])} 处未成: "
|
||||
+ "; ".join(str(x) for x in stop_r["errors"][:5]))
|
||||
|
|
@ -336,9 +337,13 @@ def _dispatch_planner(cmd_type: str, p: dict, cmd: dict) -> dict:
|
|||
exclude = _codes_with_live_plans()
|
||||
|
||||
if cmd_type == "REDUCE_EXPOSURE":
|
||||
# 减仓不撤策略腿在途买单 (exclude_strategy=True): 整体降仓 ≠ 停网格/做T (设计: 清仓
|
||||
# 才最高优先级、覆盖一切; 减仓看情况、默认不动策略)。非策略的在途买入 (自主/提议/命令)
|
||||
# 照撤 —— 正在减仓就别再往里投新钱。
|
||||
return pl.plan_reduce_exposure(
|
||||
release_amount=scale * float(p["pct"]), positions=positions,
|
||||
pending_buys=_pending_buys(), params={"weak_neg_days": sp["weak_neg_days"]},
|
||||
pending_buys=_pending_buys(exclude_strategy=True),
|
||||
params={"weak_neg_days": sp["weak_neg_days"]},
|
||||
exclude_codes=exclude)
|
||||
|
||||
if cmd_type == "INCREASE_EXPOSURE":
|
||||
|
|
@ -511,12 +516,25 @@ def refresh_progress(command_id=None) -> dict:
|
|||
|
||||
|
||||
# ================================================================ 内部助手
|
||||
def _pending_buys() -> list:
|
||||
def _pending_buys(exclude_strategy: bool = False) -> list:
|
||||
"""在途买入指令 (供 planner 生成撤单项)。
|
||||
|
||||
exclude_strategy=True 时**剔除策略腿** (origin_type='strategy') —— 减仓命令用:
|
||||
整体降仓是把仓位压下来, 不该顺手把你为个股特意设的网格 / 做T 一并掐掉。设计铁律里
|
||||
清仓 (LIQUIDATE_ALL / EXIT_STOCK / SECTOR_EXIT) 才是最高优先级、覆盖一切 (那几条仍
|
||||
传默认 False, 策略连同在途买单一律撤); 减仓看情况、默认不动策略。
|
||||
另一层原因: 减仓只发一次 (命令物化一次), 就算这里撤了策略的在途买单, 策略下一跳
|
||||
又照发一张 —— 撤了等于没撤, 只是白撤一笔、还在页面上报"撤了 N 条策略买单"误导人。"""
|
||||
rows = pms_repo.list_instructions(statuses=list(LIVE_INSTR), side="buy", limit=500)
|
||||
return [{"instruction_id": r["instruction_id"], "ts_code": r["ts_code"],
|
||||
out = []
|
||||
for r in rows:
|
||||
if exclude_strategy and r.get("origin_type") == "strategy":
|
||||
continue
|
||||
out.append({"instruction_id": r["instruction_id"], "ts_code": r["ts_code"],
|
||||
"qty": int(r.get("qty") or 0),
|
||||
"amount": float(r.get("qty") or 0) * float(r.get("limit_price") or 0),
|
||||
"side": "buy"} for r in rows]
|
||||
"side": "buy", "origin_type": r.get("origin_type")})
|
||||
return out
|
||||
|
||||
|
||||
def _pending_instructions() -> list:
|
||||
|
|
@ -675,11 +693,18 @@ def _ledger_rejects(rejects: list, command_id: str):
|
|||
def _stop_buyside_for_exit(codes: list, command_id: str) -> dict:
|
||||
"""全额清仓类命令 (一键清仓 / 清仓某股 / 清仓某行业) 下达时, 立刻停掉这些票的买入侧。
|
||||
|
||||
撤活跃策略 (网格/做T/跟踪) + 驳回待确认的买入提议。在途买单由 planner 的 HALT 撤单项
|
||||
经 _cancel_marked_instructions 另行撤销, 这里补策略与提议两层 —— 三层合起来才真正做到
|
||||
「命令一下, 这只票不再有任何新的买入」。幂等、不抛异常: 单只失败记进 errors, 不拖垮
|
||||
命令本体 (故障即守成)。撤策略只改策略状态、不动持仓, 安全。"""
|
||||
out = {"strategies": [], "proposals": [], "errors": []}
|
||||
三件事: 撤活跃策略 (网格/做T/跟踪) + 驳回待确认的买入提议 + **撤这些票当前在途的买入指令**。
|
||||
|
||||
在途买单这一层原来只有一键清仓兜得住 —— 它的 planner 会给所有在途买入排 HALT 撤单项,
|
||||
经 _cancel_marked_instructions 撤掉。但清仓某股 (EXIT_STOCK) / 清仓某行业 (SECTOR_EXIT)
|
||||
的 planner **不传 pending_buys**, 只排一条卖出, 于是「撤策略」只改了策略状态、**已经下发到
|
||||
券商的那张网格/做T 买单还挂着继续成交** —— 你正清仓这只票, 它却又买进来一笔 T+1 锁死的货。
|
||||
所以这里对被清仓的票再补一道: 直接撤掉当前在途买入指令。一键清仓时这些单多半已被 HALT
|
||||
撤过、不在在途集里, 补这道也只是空跑, 幂等无害。
|
||||
|
||||
幂等、不抛异常: 单只失败记进 errors, 不拖垮命令本体 (故障即守成)。撤策略只改策略状态、
|
||||
撤在途买单走 executor.cancel_instruction (下游没撤成不硬标终态), 都不动持仓, 安全。"""
|
||||
out = {"strategies": [], "proposals": [], "instructions": [], "errors": []}
|
||||
codeset = {c for c in (codes or []) if c}
|
||||
if not codeset:
|
||||
return out
|
||||
|
|
@ -719,9 +744,30 @@ def _stop_buyside_for_exit(codes: list, command_id: str) -> dict:
|
|||
out["proposals"].append(p["proposal_id"])
|
||||
except Exception as e:
|
||||
out["errors"].append(f"{p.get('ts_code')} 驳回买入提议失败: {type(e).__name__}: {e}")
|
||||
if out["strategies"] or out["proposals"]:
|
||||
logger.warning("[命令] %s 清仓覆盖冲突: 撤策略 %s, 驳回买入提议 %s",
|
||||
command_id, out["strategies"], out["proposals"])
|
||||
# 第三层: 撤这些票当前在途的买入指令 (EXIT_STOCK/SECTOR_EXIT 的 planner 不排 HALT, 全靠这里)。
|
||||
# 只查当前在途买入, 撤过的/已终态的不会回到这个集里, 所以一键清仓已 HALT 掉的这里空跑、不重撤。
|
||||
from app.services import executor
|
||||
try:
|
||||
live_buys = [i for i in pms_repo.list_instructions(statuses=list(LIVE_INSTR), side="buy",
|
||||
limit=500)
|
||||
if i.get("ts_code") in codeset]
|
||||
except Exception as e:
|
||||
live_buys = []
|
||||
out["errors"].append(f"读在途买单失败: {type(e).__name__}: {e}")
|
||||
for i in live_buys:
|
||||
iid = i.get("instruction_id")
|
||||
try:
|
||||
r = executor.cancel_instruction(iid, reason="清仓命令下达: 撤该票在途买入 (命令覆盖冲突)") or {}
|
||||
if r.get("ok"):
|
||||
out["instructions"].append(iid)
|
||||
else:
|
||||
# 下游没撤成: 指令仍在途, 单独列出来别吞 (与 _cancel_marked_instructions 同纪律)
|
||||
out["errors"].append(f"{i.get('ts_code')} 撤在途买单 {iid} 未成: {r.get('error') or r.get('message')}")
|
||||
except Exception as e:
|
||||
out["errors"].append(f"{i.get('ts_code')} 撤在途买单 {iid} 失败: {type(e).__name__}: {e}")
|
||||
if out["strategies"] or out["proposals"] or out["instructions"]:
|
||||
logger.warning("[命令] %s 清仓覆盖冲突: 撤策略 %s, 驳回买入提议 %s, 撤在途买单 %s",
|
||||
command_id, out["strategies"], out["proposals"], out["instructions"])
|
||||
return out
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -72,10 +72,15 @@ def _post(url: str, payload: dict, timeout: int) -> dict:
|
|||
return r.json() or {}
|
||||
|
||||
|
||||
def _strategy_immediate(*, side, now, day, quota, fired_today) -> dict:
|
||||
def _strategy_immediate(*, side, now, day, quota, fired_today,
|
||||
is_last_day: bool = True, urgent: bool = False) -> dict:
|
||||
"""网格/跟踪止盈的到价即成交决策: 只做硬事实检查(配额/时段/停牌/一字板), 不做 vwap 择时,
|
||||
不做 14:45 强制。真正的合规(单股上限/T+1/暂停买入等)由 run_tick 随后的 rule_gate 终检兜底。
|
||||
这样网格是"到档位价才成交、当日没成交则由窗口收口作废", 不会像命令那样被收盘强平成市价单。"""
|
||||
这样网格是"到档位价才成交、当日没成交则由窗口收口作废", 不会像命令那样被收盘强平成市价单。
|
||||
|
||||
is_last_day 默认 True: 网格/跟踪止盈都是 window_tdays=1 的短窗口单, run_tick 一律按在途口径
|
||||
(_inflight_today) 扣当日投放量, 所以这里 left<=0 = 该出的档全挂着等成交, 文案跟着走
|
||||
quota_wait_reason 那句"已全部在途", 而不是误报"当日配额已出完"。"""
|
||||
left = max(0, int(quota) - int(fired_today))
|
||||
now_min = et.hm_to_min(now)
|
||||
|
||||
|
|
@ -83,7 +88,7 @@ def _strategy_immediate(*, side, now, day, quota, fired_today) -> dict:
|
|||
return {"action": a, "qty_hint": left, "limit_price": limit, "reason": r, "source": "策略即时"}
|
||||
|
||||
if left <= 0:
|
||||
return _o(et.ACT_WAIT, "当日配额已出完")
|
||||
return _o(et.ACT_WAIT, et.quota_wait_reason(urgent, is_last_day))
|
||||
if not et.in_session(now_min):
|
||||
return _o(et.ACT_WAIT, "非交易时段")
|
||||
price = _f(day.get("price"))
|
||||
|
|
@ -114,7 +119,8 @@ def decide(*, side: str, action: str, ts_code: str, now, day: dict, params: dict
|
|||
# 网格/跟踪止盈: 策略层已按档位触发, 这里不做择时博弈, 到价即时成交、收盘不强制。
|
||||
# 只识别策略来源且类型为 GRID/TRAIL 的指令; 做T(T0)与其余来源一律走原有逻辑, 不受影响。
|
||||
if (prog or {}).get("origin") == "strategy" and str((prog or {}).get("stype")) in ("GRID", "TRAIL"):
|
||||
return _strategy_immediate(side=side, now=now, day=day, quota=quota, fired_today=fired_today)
|
||||
return _strategy_immediate(side=side, now=now, day=day, quota=quota, fired_today=fired_today,
|
||||
is_last_day=is_last_day, urgent=urgent)
|
||||
|
||||
if not available():
|
||||
d = et.decide(side=side, now=now, day=day, params=params, is_last_day=is_last_day,
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@
|
|||
test_batch14_units.py 宏观择时: 指数计算与对齐/区域迟滞/周期与对数映射/
|
||||
分方向触发/让路与冷却/宏观闸/建议采纳 (25 例)
|
||||
test_batch15_units.py 紧急清仓修复: 配额在途口径不重复扣/紧急清仓不被
|
||||
误判配额已出完/清仓命令即时撤策略与买入提议 (7 例)
|
||||
误判配额已出完/清仓命令即时撤策略/买入提议/在途买单;
|
||||
在途与配额文案拆分; 减仓不掐策略腿 (12 例)
|
||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
||||
共 505 例
|
||||
共 510 例
|
||||
任一子集失败即整体失败 (退出码 1)。
|
||||
"""
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@
|
|||
本批覆盖:
|
||||
* _inflight_today 只算在途、已终态一律不算 (甲的口径修正);
|
||||
* 同一批 children 下, _consumed_today 与 _inflight_today 的差, 以及内置择时在两种口径下
|
||||
FIRE / 配额已出完 的分野 (直接钉死根因, 用真函数不打桩);
|
||||
* _stop_buyside_for_exit 撤策略 + 驳回买入提议, 不动卖出提议与无关票 (乙);
|
||||
* plan_command 下 LIQUIDATE_ALL 时确实撤掉了持仓票的活跃策略 (乙的接线, 真实走一遍).
|
||||
FIRE / WAIT 的分野 (直接钉死根因, 用真函数不打桩);
|
||||
* _stop_buyside_for_exit 撤策略 + 驳回买入提议 + 撤在途买单, 不动卖出提议与无关票 (乙);
|
||||
* plan_command 下 LIQUIDATE_ALL 时确实撤掉了持仓票的活跃策略 (乙的接线, 真实走一遍);
|
||||
* 丙(文案拆分): 紧急/末日单 left<=0 报"已全部在途, 等成交回来", 普通多日单才报"当日配额已出完",
|
||||
hard_gate 与策略即时两条路各测一遍;
|
||||
* 丁(减仓看情况): _pending_buys(exclude_strategy=True) 剔掉策略腿 —— 减仓不掐网格/做T,
|
||||
清仓与全局暂停买入仍全撤.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
|
@ -110,11 +114,13 @@ def _():
|
|||
new = executor._inflight_today(children, YMD) # 0 + 900 = 900
|
||||
assert old == 1800 and new == 900, (old, new)
|
||||
|
||||
# 老口径: left = 1400 - 1800 < 0 → "当日配额已出完", 紧急也被挡 (bug)
|
||||
# 老口径: left = 1400 - 1800 < 0 → 直接 WAIT, 紧急清仓被挡在这道判断前 (bug)。
|
||||
# (文案这批已拆分: 紧急/末日单 left<=0 报"已全部在途", 普通多日单才报"当日配额已出完";
|
||||
# 这里 urgent=True 走前者。要钉的是 action=WAIT —— 老口径把紧急清仓 benched 了。)
|
||||
d_old = et.decide(side="sell", now=NOW, day=DAY, params=EXEC_PRM,
|
||||
is_last_day=True, is_command=True, urgent=True,
|
||||
fired_today=old, quota=quota)
|
||||
assert d_old["action"] == et.ACT_WAIT and "配额已出完" in d_old["reason"], d_old
|
||||
assert d_old["action"] == et.ACT_WAIT and "在途" in d_old["reason"], d_old
|
||||
# 新口径: left = 1400 - 900 = 500 > 0 → 紧急直通 FIRE, 还能再投 500
|
||||
d_new = et.decide(side="sell", now=NOW, day=DAY, params=EXEC_PRM,
|
||||
is_last_day=True, is_command=True, urgent=True,
|
||||
|
|
@ -158,15 +164,18 @@ class _Rec:
|
|||
self.proposals = []
|
||||
self.cancelled = []
|
||||
self.declined = []
|
||||
self.cancelled_ins = [] # 被撤的在途买入指令 id
|
||||
self.ledger = []
|
||||
|
||||
|
||||
def _patch_stop(rec, strategies, proposals):
|
||||
from app.services import strategy_service
|
||||
def _patch_stop(rec, strategies, proposals, instructions=()):
|
||||
from app.services import strategy_service, executor as _ex
|
||||
rec._orig = (pms_repo.list_strategies, pms_repo.list_proposals, pms_repo.decide_proposal,
|
||||
pms_repo.insert_ledger, strategy_service.set_status)
|
||||
pms_repo.insert_ledger, pms_repo.list_instructions,
|
||||
strategy_service.set_status, _ex.cancel_instruction)
|
||||
pms_repo.list_strategies = lambda **kw: list(strategies)
|
||||
pms_repo.list_proposals = lambda **kw: list(proposals)
|
||||
pms_repo.list_instructions = lambda **kw: list(instructions)
|
||||
|
||||
def _decide(pid, decision):
|
||||
rec.declined.append((pid, decision))
|
||||
|
|
@ -179,14 +188,20 @@ def _patch_stop(rec, strategies, proposals):
|
|||
return {"ok": True, "status": status}
|
||||
strategy_service.set_status = _setst
|
||||
|
||||
def _cancel_ins(iid, reason="页面人工撤销"):
|
||||
rec.cancelled_ins.append(iid)
|
||||
return {"ok": True}
|
||||
_ex.cancel_instruction = _cancel_ins
|
||||
|
||||
|
||||
def _unpatch_stop(rec):
|
||||
from app.services import strategy_service
|
||||
from app.services import strategy_service, executor as _ex
|
||||
(pms_repo.list_strategies, pms_repo.list_proposals, pms_repo.decide_proposal,
|
||||
pms_repo.insert_ledger, strategy_service.set_status) = rec._orig
|
||||
pms_repo.insert_ledger, pms_repo.list_instructions,
|
||||
strategy_service.set_status, _ex.cancel_instruction) = rec._orig
|
||||
|
||||
|
||||
@case("命令覆盖冲突·撤活跃策略 + 驳回买入提议, 不动卖出提议与无关票")
|
||||
@case("命令覆盖冲突·撤活跃策略 + 驳回买入提议 + 撤在途买单, 不动卖出提议与无关票")
|
||||
def _():
|
||||
rec = _Rec()
|
||||
strategies = [{"strategy_id": "STR_A", "ts_code": "002128.SZ", "type": "GRID"},
|
||||
|
|
@ -194,13 +209,18 @@ def _():
|
|||
proposals = [{"proposal_id": "P_buy", "ts_code": "002128.SZ", "action": "DCA"}, # 买, 驳
|
||||
{"proposal_id": "P_sell", "ts_code": "002128.SZ", "action": "TRIM"}, # 卖, 留
|
||||
{"proposal_id": "P_other", "ts_code": "600000.SH", "action": "OPEN"}] # 无关票
|
||||
_patch_stop(rec, strategies, proposals)
|
||||
# 在途买单: 本票一张 (要撤) + 无关票一张 (要留)
|
||||
instructions = [{"instruction_id": "INS_buy", "ts_code": "002128.SZ", "side": "buy"},
|
||||
{"instruction_id": "INS_other", "ts_code": "600000.SH", "side": "buy"}]
|
||||
_patch_stop(rec, strategies, proposals, instructions=instructions)
|
||||
try:
|
||||
out = csvc._stop_buyside_for_exit(["002128.SZ"], "CMD_T_0001")
|
||||
assert out["strategies"] == ["STR_A"], out # 只撤本票策略
|
||||
assert out["proposals"] == ["P_buy"], out # 只驳本票买入提议
|
||||
assert out["instructions"] == ["INS_buy"], out # 只撤本票在途买单
|
||||
assert rec.cancelled == [("STR_A", "CANCELLED", "command")], rec.cancelled
|
||||
assert rec.declined == [("P_buy", "DECLINED")], rec.declined
|
||||
assert rec.cancelled_ins == ["INS_buy"], rec.cancelled_ins # 无关票在途买单不动
|
||||
assert any(x.get("action") == "CLEANUP" for x in rec.ledger) # 留痕
|
||||
assert not out["errors"], out
|
||||
finally:
|
||||
|
|
@ -210,11 +230,12 @@ def _():
|
|||
@case("命令覆盖冲突·空票集不动任何东西")
|
||||
def _():
|
||||
rec = _Rec()
|
||||
_patch_stop(rec, [{"strategy_id": "STR_A", "ts_code": "002128.SZ"}], [])
|
||||
_patch_stop(rec, [{"strategy_id": "STR_A", "ts_code": "002128.SZ"}], [],
|
||||
instructions=[{"instruction_id": "INS_x", "ts_code": "002128.SZ", "side": "buy"}])
|
||||
try:
|
||||
out = csvc._stop_buyside_for_exit([], "CMD_T_0002")
|
||||
assert out == {"strategies": [], "proposals": [], "errors": []}, out
|
||||
assert not rec.cancelled
|
||||
assert out == {"strategies": [], "proposals": [], "instructions": [], "errors": []}, out
|
||||
assert not rec.cancelled and not rec.cancelled_ins
|
||||
finally:
|
||||
_unpatch_stop(rec)
|
||||
|
||||
|
|
@ -262,6 +283,63 @@ def _():
|
|||
csvc.param_store.get_int) = orig
|
||||
|
||||
|
||||
# ================================================================ 丙: 文案拆分 (在途 / 配额已出完)
|
||||
@case("文案拆分·quota_wait_reason: 紧急或末日报'已全部在途', 普通多日报'当日配额已出完'")
|
||||
def _():
|
||||
assert et.quota_wait_reason(urgent=True, is_last_day=False) == "已全部在途, 等成交回来"
|
||||
assert et.quota_wait_reason(urgent=False, is_last_day=True) == "已全部在途, 等成交回来"
|
||||
assert et.quota_wait_reason(urgent=False, is_last_day=False) == "当日配额已出完"
|
||||
|
||||
|
||||
@case("文案拆分·普通多日单 left<=0 仍报'当日配额已出完' (行为不变)")
|
||||
def _():
|
||||
d = et.decide(side="sell", now=NOW, day=DAY, params=EXEC_PRM,
|
||||
is_last_day=False, is_command=True, urgent=False,
|
||||
fired_today=1000, quota=1000) # 今日已投放=配额 → left=0
|
||||
assert d["action"] == et.ACT_WAIT and d["reason"] == "当日配额已出完", d
|
||||
|
||||
|
||||
@case("文案拆分·紧急/末日单 left<=0 改报'已全部在途, 等成交回来'")
|
||||
def _():
|
||||
d = et.decide(side="sell", now=NOW, day=DAY, params=EXEC_PRM,
|
||||
is_last_day=True, is_command=True, urgent=True,
|
||||
fired_today=1000, quota=1000) # 该出的全挂在券商, left=0
|
||||
assert d["action"] == et.ACT_WAIT and d["reason"] == "已全部在途, 等成交回来", d
|
||||
|
||||
|
||||
@case("文案拆分·策略即时(网格)同样分口径: 末日报在途, 非末日报配额到顶")
|
||||
def _():
|
||||
from app.services import exec_advisor
|
||||
# 网格是 window=1 末日单, left<=0 = 该出的档都挂着等成交
|
||||
d1 = exec_advisor._strategy_immediate(side="buy", now=NOW, day=DAY, quota=100,
|
||||
fired_today=100, is_last_day=True)
|
||||
assert d1["action"] == et.ACT_WAIT and d1["reason"] == "已全部在途, 等成交回来", d1
|
||||
# 退一步防串味: 若某笔不是末日单, 该报配额到顶
|
||||
d2 = exec_advisor._strategy_immediate(side="buy", now=NOW, day=DAY, quota=100,
|
||||
fired_today=100, is_last_day=False, urgent=False)
|
||||
assert d2["reason"] == "当日配额已出完", d2
|
||||
|
||||
|
||||
# ================================================================ 丁: 减仓不撤策略腿在途买单
|
||||
@case("减仓口径·_pending_buys(exclude_strategy=True) 剔策略腿, 默认(清仓/暂停买入)全带")
|
||||
def _():
|
||||
orig = pms_repo.list_instructions
|
||||
rows = [{"instruction_id": "I_auto", "ts_code": "600000.SH", "qty": 100,
|
||||
"limit_price": 10.0, "origin_type": "plan"},
|
||||
{"instruction_id": "I_prop", "ts_code": "600000.SH", "qty": 200,
|
||||
"limit_price": 10.0, "origin_type": "proposal"},
|
||||
{"instruction_id": "I_grid", "ts_code": "002128.SZ", "qty": 300,
|
||||
"limit_price": 27.6, "origin_type": "strategy"}]
|
||||
try:
|
||||
pms_repo.list_instructions = lambda **kw: list(rows)
|
||||
keep_all = {b["instruction_id"] for b in csvc._pending_buys()}
|
||||
no_strat = {b["instruction_id"] for b in csvc._pending_buys(exclude_strategy=True)}
|
||||
assert keep_all == {"I_auto", "I_prop", "I_grid"}, keep_all # 清仓/全局暂停买入: 全撤
|
||||
assert no_strat == {"I_auto", "I_prop"}, no_strat # 减仓: 网格/做T 腿留着继续跑
|
||||
finally:
|
||||
pms_repo.list_instructions = orig
|
||||
|
||||
|
||||
# ================================================================ runner
|
||||
def main():
|
||||
passed = failed = 0
|
||||
|
|
|
|||
Loading…
Reference in New Issue