三源合议工作包三(二):策略层两道技术面离场(part 3+4)
part3 跟踪止盈盘中 SAR 止损线:09:45 后现价跌破昨日 SAR×(1-0.003) 即全清、当日一次; 每天 09:40 把技术面映射的 SAR 值刷进每条自动跟踪止盈,无读数撤旧线。 part4 弱基本面试探仓紧止盈自动挂载:基本面看空+技术面看多的试探仓次日 09:40 自动挂 回撤 3%/硬目标 8%/带 SAR 线的跟踪止盈,不占每日名额;认它靠入场账本 advice.tight_trail。 至此工作包三四部分全做完。台账 011;第三十批扩到 33 例;开发机全量 ALL SUITES PASS。 只对自动跟踪止盈与弱基本面试探仓生效,其余策略票行为不变(batch17/19 回归绿)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
85f4330c59
commit
2d019a1b17
28
DEVLOG.md
28
DEVLOG.md
|
|
@ -1433,6 +1433,34 @@ app/core/action_engine.py(eval_tech_exit、SRC_TECH_EXIT、EVALUATORS 加一
|
|||
|
||||
工作包三剩两部分:跟踪止盈策略加盘中 SAR 止损线(09:45 后现价低于昨日 SAR 千分之三即卖、当日一次,每天 09:40 把映射里的 SAR 刷进自动挂载的跟踪止盈);弱基本面加技术面看多的试探仓次日 09:40 自动挂回撤 3%、硬目标 8%、带 SAR 线的紧止盈(台账 011)。工作包四选股打分另一仓库单独审批。工作包五文档。
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-11 · 工作包三离场纪律(part 3+4):策略层的两道技术面离场
|
||||
|
||||
**做了什么**
|
||||
|
||||
补齐工作包三的后两部分,都在策略层。至此工作包三四部分全做完。
|
||||
|
||||
一,跟踪止盈加盘中 SAR 止损线。跟踪止盈评估器加一道 SAR 止损:09:45 后现价跌破昨日 SAR 值的千分之三缓冲即全清,当日只触发一次。SAR 值由 09:40 的策略自动挂载一跳刷进每条自动挂载的跟踪止盈的参数。取不到读数就撤掉旧的 SAR 线,不拿旧读数当今天。没刷进 SAR 线时这道线不判,加不改。
|
||||
|
||||
二,弱基本面试探仓的紧止盈自动挂载。基本面看空加技术面看多的试探仓,次日 09:40 自动挂一条回撤百分之三、硬目标百分之八、带 SAR 线的跟踪止盈,不占每日新挂名额。认它靠入场账本里 advice.tight_trail 这个标记(仓位矩阵 advise_v2 给这类试探仓打的),顺着首批未平批次到指令到账本放行记录找。冷却与在途照常让路。
|
||||
|
||||
**动了哪些文件**
|
||||
|
||||
app/services/strategy_runner.py(跟踪止盈评估器 _eval_trail 加盘中 SAR 止损线,防了单测传 now=None 的空指针);app/services/strategy_advisor.py(09:40 一跳加 _refresh_sar_lines 刷 SAR 线;_entry_tight_trail 认入场标记、_attach_tight_trail 挂紧止盈,接进主循环无策略票走常规边之前);app/services/param_store.py(登记 PMS_TECH_SAR_STOP_ON_TRAIL、PMS_TECH_SAR_STOP_BUFFER、PMS_TECH_TIGHT_TRAIL_GIVEBACK、PMS_TECH_TIGHT_TRAIL_TARGET,三个加校验范围);scripts/test_batch30_units.py(part3 九例、part4 六例,共扩到 33 例);scripts/run_tests.py(例数与总数 820);docs/复盘决定台账.md(台账 011)。
|
||||
|
||||
**部署方式**
|
||||
|
||||
改了 Python,make deploy 重建镜像加 force-recreate。收盘后 155,已授权直接部署。
|
||||
|
||||
**真机判收**
|
||||
|
||||
待部署后补。开发机全量 ALL SUITES PASS(第三十批 33 例,含每组开关关掉与没读数时不生效的守)。这两部分只对自动挂载的跟踪止盈与弱基本面试探仓生效,其余策略票行为不变(batch17/19 回归绿)。
|
||||
|
||||
**还欠着什么**
|
||||
|
||||
工作包三四部分全做完。剩:工作包四选股打分在 akg-factor-bridge 另一仓库,单独审批后再动;页面管理视图持仓总览「证据列印两遍」的显示错误与研究面列本轮未碰。真机判收要看真有弱基本面试探仓入场次日的紧止盈自动挂载、以及策略票 SAR 翻空当天的止损。
|
||||
|
||||
---
|
||||
<!--
|
||||
下一条节点从这里往下写,格式照抄上面:
|
||||
|
|
|
|||
|
|
@ -120,6 +120,12 @@ RUNTIME_EXTRA = {
|
|||
"PMS_TECH_EXIT_AUTONOMY": ("full", str, "技术面转空离场档位: off 不评 / propose_only 交人确认 / full 自动执行 (与保垫减仓同档, 按拍板默认)"),
|
||||
"PMS_TECH_EXIT_TRIM_RATIO": (0.3333, float, "转空未确认时的减持比例 (占总持仓; 确认转空则清仓全部可卖量; 默认三分之一)"),
|
||||
"PMS_TECH_EXIT_DONE": ("", str, "本次翻空已处理过的代码集 (scan_and_route 写入的 JSON: codes/at; 跨轮去重, 页面只读, 勿手改)"),
|
||||
# 跟踪止盈的盘中 SAR 止损线 (2026-09-11 工作包三 part 3): 每天 09:40 把技术面 SAR 值刷进自动跟踪止盈, 盘中跌破即卖。
|
||||
"PMS_TECH_SAR_STOP_ON_TRAIL": (True, bool, "自动跟踪止盈加盘中 SAR 止损线: 09:45 后现价跌破昨日 SAR 值的缓冲即全清, 当日一次 (关掉即不刷不判)"),
|
||||
"PMS_TECH_SAR_STOP_BUFFER": (0.003, float, "SAR 止损线的跌破缓冲 (现价低于 SAR×(1-缓冲) 才触发; 默认千分之三)"),
|
||||
# 弱基本面试探仓的紧止盈自动挂载 (2026-09-11 工作包三 part 4, 台账 011): 基本面看空加技术面看多的试探仓次日自动挂。
|
||||
"PMS_TECH_TIGHT_TRAIL_GIVEBACK": (0.03, float, "弱基本面试探仓紧止盈的回撤比例 (从高点回落这么多即全清; 默认百分之三)"),
|
||||
"PMS_TECH_TIGHT_TRAIL_TARGET": (0.08, float, "弱基本面试探仓紧止盈的硬目标 (浮盈到这么多直接全清; 默认百分之八)"),
|
||||
}
|
||||
|
||||
# **读不到时必须按"已暂停"处理的键 (fail-closed)。**
|
||||
|
|
@ -498,7 +504,8 @@ _RANGES = {
|
|||
"PMS_TECH_FLIP_FRESH_DAYS": (1, 10),
|
||||
# 三源合议 (2026-09-11)
|
||||
"PMS_FUND_STALE_DAYS": (0, 3650), "PMS_FUND_CONSENSUS_GOOD_MIN": (0, 10),
|
||||
"PMS_TECH_EXIT_TRIM_RATIO": (0, 1),
|
||||
"PMS_TECH_EXIT_TRIM_RATIO": (0, 1), "PMS_TECH_SAR_STOP_BUFFER": (0, 0.1),
|
||||
"PMS_TECH_TIGHT_TRAIL_GIVEBACK": (0.005, 0.5), "PMS_TECH_TIGHT_TRAIL_TARGET": (0.01, 1),
|
||||
# 宏观择时层
|
||||
"PMS_MACRO_HOT_TH": (0, 100), "PMS_MACRO_COLD_TH": (-100, 0),
|
||||
"PMS_MACRO_EXIT_BAND": (0, 100), "PMS_MACRO_CONFIRM_DAYS": (1, 10),
|
||||
|
|
|
|||
|
|
@ -480,6 +480,15 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
|
|||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
# 盘中 SAR 止损线的每日刷新 (2026-09-11 工作包三 part 3): 把技术面映射里的 SAR 值刷进每条
|
||||
# 自动挂载的跟踪止盈的 params.sar_line, 供 strategy_runner._eval_trail 的盘中 SAR 止损用。
|
||||
# 开关 PMS_TECH_SAR_STOP_ON_TRAIL 关掉不刷; 试算不写。取不到读数就撤掉旧 SAR 线 (不拿旧读数当今天)。
|
||||
if param_store.get_bool("PMS_TECH_SAR_STOP_ON_TRAIL", True) and not dry_run:
|
||||
try:
|
||||
_refresh_sar_lines(strat_all, out)
|
||||
except Exception as e: # noqa: BLE001 —— 刷 SAR 线失败不拖垮整轮挂载
|
||||
out["errors"].append(f"SAR 线刷新失败: {type(e).__name__}: {e}")
|
||||
|
||||
# ---- 逐票走状态机 ----
|
||||
for p in held:
|
||||
out["checked"] += 1
|
||||
|
|
@ -504,6 +513,10 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
|
|||
_tend_existing(st, p, a, fresh, hv, prm, buypause, handoff_cool,
|
||||
today, dry_run, out, strategy_service, accum_ok=accum_ok)
|
||||
continue
|
||||
# 弱基本面试探仓的紧止盈自动挂载 (2026-09-11 工作包三 part 4): 无策略票里, 基本面看空加
|
||||
# 技术面看多的试探仓次日自动挂紧止盈, 不占每日名额。挂了 (或占位让路) 就不再走常规边。
|
||||
if _attach_tight_trail(p, prm, live_codes, optout, dry_run, out, strategy_service):
|
||||
continue
|
||||
edge, why = plan_edge(cls=a.get("cls"), fresh=fresh, heat=hv,
|
||||
cushion=p.get("cushion_pct"), prm=prm)
|
||||
if not edge:
|
||||
|
|
@ -521,6 +534,122 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
|
|||
return _record_scan(out, dry_run, now)
|
||||
|
||||
|
||||
def _refresh_sar_lines(strat_all, out) -> None:
|
||||
"""把技术面映射里的 SAR 值刷进每条自动挂载的跟踪止盈的 params.sar_line (2026-09-11 工作包三 part 3)。
|
||||
|
||||
只动自动挂载 (note 以「自动挂载」开头) 且 ACTIVE 的跟踪止盈; 人工策略与网格不碰。
|
||||
取不到某票的 SAR (无读数、映射停更) 就撤掉它旧的 sar_line —— 不拿旧读数当今天的止损线。
|
||||
值没变就不写库。写失败只记 errors, 不拦整轮。
|
||||
"""
|
||||
from app.services import tech_service
|
||||
tmap = tech_service.state_map() # 取不到抛给调用方的 try 兜住
|
||||
n = 0
|
||||
for s in strat_all:
|
||||
if str(s.get("type") or "").upper() != "TRAIL":
|
||||
continue
|
||||
if not str(s.get("note") or "").startswith("自动挂载"):
|
||||
continue
|
||||
if str(s.get("status") or "ACTIVE").upper() != "ACTIVE":
|
||||
continue
|
||||
params = dict(s.get("params") or {})
|
||||
old = params.get("sar_line")
|
||||
sar = _f((tmap.get(s.get("ts_code")) or {}).get("sar_value"))
|
||||
if sar and sar > 0:
|
||||
params["sar_line"] = round(sar, 3)
|
||||
else:
|
||||
params.pop("sar_line", None) # 无读数: 撤掉旧线, 这道 SAR 止损本日不生效
|
||||
if params.get("sar_line") != old:
|
||||
if pms_repo.update_strategy(s["strategy_id"], params=params):
|
||||
n += 1
|
||||
else:
|
||||
out["errors"].append(f"{s.get('ts_code')} SAR 线刷新影响 0 行")
|
||||
if n:
|
||||
out["sar_refreshed"] = n
|
||||
|
||||
|
||||
def _entry_tight_trail(code) -> bool:
|
||||
"""这只持仓是不是「基本面看空加技术面看多的试探仓」(2026-09-11 工作包三 part 4)。
|
||||
|
||||
判据是入场那条账本记录里 advice.tight_trail 为真 —— 那是 advise_v2 给这类试探仓打的标记
|
||||
(方案附录乙: 只给试探仓一批、交人、配紧止盈)。顺着首批未平批次 → 指令 → 账本放行记录找。
|
||||
读不到、认不出一律按否 —— 认不出就不自动挂紧止盈, 交给常规边或人工 (加不改)。
|
||||
"""
|
||||
try:
|
||||
lots = pms_repo.list_lots(code, status="OPEN", limit=50)
|
||||
if not lots:
|
||||
return False
|
||||
iid = lots[0].get("instruction_id")
|
||||
if not iid:
|
||||
return False
|
||||
refs = [str(iid)]
|
||||
ins = pms_repo.get_instruction(str(iid)) or {}
|
||||
for k in (ins.get("origin_id"), (ins.get("progress") or {}).get("from_proposal")):
|
||||
if k and str(k) not in refs:
|
||||
refs.append(str(k))
|
||||
for r in pms_repo.ledger_by_ref(refs):
|
||||
if r.get("verdict") != "PASS":
|
||||
continue
|
||||
adv = (r.get("hard_numbers") or {}).get("advice")
|
||||
if isinstance(adv, dict) and adv.get("tight_trail"):
|
||||
return True
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("[自动挂载] 查入场紧止盈标记失败 %s (按否): %s", code, e)
|
||||
return False
|
||||
|
||||
|
||||
def _attach_tight_trail(p, prm, live_codes, optout, dry_run, out, strategy_service) -> bool:
|
||||
"""弱基本面试探仓的紧止盈自动挂载 (2026-09-11 工作包三 part 4, 台账 011)。
|
||||
|
||||
基本面看空加技术面看多的试探仓, 次日 09:40 自动挂一条回撤 3%、硬目标 8%、带 SAR 线的跟踪止盈,
|
||||
**不占每日新挂名额** (它是给弱基本面试探仓配的保护, 不是常规吸筹/高热挂载)。挂上后这只票就有
|
||||
策略了, 下一跳 st 不为空、不再重挂。返回 True 表示「这只票已按紧止盈处理」(占位, 不再走常规边)。
|
||||
|
||||
冷却与在途照常让路: 人工撤下过就等冷却, 有在途就缓一天。挂载参数与 SAR 线由本函数生成,
|
||||
SAR 值取当轮技术面映射; 取不到就不带 SAR 线 (回撤与硬目标照常, 加不改)。
|
||||
"""
|
||||
code = p["ts_code"]
|
||||
if not _entry_tight_trail(code):
|
||||
return False
|
||||
if (code, R_TRAIL) in optout:
|
||||
out["skipped"].append({"ts_code": code, "why":
|
||||
f"弱基本面试探仓想挂紧止盈, 但人工撤下同类冷却期内 "
|
||||
f"({prm['optout_cooldown_tdays']} 个交易日)"})
|
||||
return True
|
||||
if code in live_codes:
|
||||
out["skipped"].append({"ts_code": code, "why": "弱基本面试探仓想挂紧止盈, 但有在途, 缓到下一个扫描日"})
|
||||
return True
|
||||
gb = param_store.get_float("PMS_TECH_TIGHT_TRAIL_GIVEBACK", 0.03)
|
||||
ht = param_store.get_float("PMS_TECH_TIGHT_TRAIL_TARGET", 0.08)
|
||||
params = {"giveback": gb, "sell_ratio": 1.0, "hard_target": ht}
|
||||
try:
|
||||
from app.services import tech_service
|
||||
sar = _f((tech_service.state_map().get(code) or {}).get("sar_value"))
|
||||
if sar and sar > 0:
|
||||
params["sar_line"] = round(sar, 3)
|
||||
except Exception: # noqa: BLE001 —— 取不到 SAR 不拦, 回撤与硬目标照常
|
||||
pass
|
||||
note = (f"{NOTE_AUTO}基本面看空加技术面看多的试探仓 → 紧止盈 "
|
||||
f"回撤 {gb:.0%} 硬目标 {ht:.0%}" + ("、带 SAR 线" if params.get("sar_line") else ""))
|
||||
if dry_run:
|
||||
out["attached"].append({"ts_code": code, "type": "TRAIL", "dry_run": True,
|
||||
"why": "弱基本面试探仓紧止盈 (不占每日名额)", "params": params})
|
||||
return True
|
||||
r = strategy_service.attach({"ts_code": code, "type": "TRAIL", "autonomy": "auto",
|
||||
"params": params, "note": note[:280]}, by="auto") or {}
|
||||
if not r.get("ok"):
|
||||
errs = "; ".join(str(x) for x in (r.get("errors") or ["挂载校验未过"]))
|
||||
out["blocked"].append({"ts_code": code, "edge": R_TRAIL, "why": errs})
|
||||
_ledger(code, "NOTE", f"想挂弱基本面紧止盈被挂载校验挡下: {errs}",
|
||||
{"price": p.get("price")}, None, out)
|
||||
return True
|
||||
out["attached"].append({"ts_code": code, "type": "TRAIL", "strategy_id": r.get("strategy_id"),
|
||||
"why": "弱基本面试探仓紧止盈 (不占每日名额)"})
|
||||
_ledger(code, "PASS", note[:200], {"price": p.get("price"), "sar_line": params.get("sar_line")},
|
||||
r.get("strategy_id"), out, action="ATTACH")
|
||||
logger.warning("[自动挂载] %s 挂弱基本面紧止盈: 回撤 %.0f%% 硬目标 %.0f%%", code, gb * 100, ht * 100)
|
||||
return True
|
||||
|
||||
|
||||
def _tend_existing(st, p, a, fresh, hv, prm, buypause, handoff_cool, today,
|
||||
dry_run, out, strategy_service, accum_ok=True):
|
||||
"""已挂策略的票: 边三 (派发停买/回明确恢复) 与 边四 (接力切换)。"""
|
||||
|
|
|
|||
|
|
@ -526,6 +526,21 @@ def _eval_trail(st, pos, day, now, ctx):
|
|||
# 全清数量: 科创板持仓不足 200 股时按交易所例外一次性全卖, 否则整百
|
||||
return avail if avail < mlot else (_round_lot(avail) or avail)
|
||||
|
||||
# 盘中 SAR 止损线 (2026-09-11 工作包三 part 3): 09:45 后现价跌破昨日 SAR 值的缓冲即全清, 当日只触发一次。
|
||||
# SAR 值由 09:40 的 strategy_advisor.scan 刷进 params["sar_line"] (工作包三 part 3 的刷新腿);
|
||||
# 没刷进 (技术面无读数、首日、映射停更) 就跳过这道线 —— 加不改, 拿不到 SAR 绝不当"跌破"。
|
||||
# 与 giveback/hard_target 并列的一道硬止损, 不看 armed (SAR 翻空是趋势破位, 不必先武装)。
|
||||
sar_line = _f(params.get("sar_line"))
|
||||
if sar_line > 0 and now is not None and param_store.get_bool("PMS_TECH_SAR_STOP_ON_TRAIL", True):
|
||||
hm = now.hour * 100 + now.minute
|
||||
if hm >= 945 and int(state.get("sar_stop_day") or 0) != td.ymd(now):
|
||||
buf = param_store.get_float("PMS_TECH_SAR_STOP_BUFFER", 0.003)
|
||||
if price <= sar_line * (1 - buf):
|
||||
state["sar_stop_day"] = td.ymd(now) # 当日只触发一次
|
||||
return {"side": "sell", "action": A_EXIT, "qty": _all_out(), "leg": "trail_sar",
|
||||
"reason": f"跟踪止盈-SAR 止损: 现价 {price} 跌破昨日 SAR {sar_line}"
|
||||
f"×(1-{buf:.1%}), 全清 {_all_out()} 股"}
|
||||
|
||||
# 硬止盈目标: 直接全清
|
||||
if hard_target > 0 and profit >= hard_target:
|
||||
return {"side": "sell", "action": A_EXIT, "qty": _all_out(), "leg": "trail_hard",
|
||||
|
|
|
|||
|
|
@ -174,4 +174,16 @@
|
|||
|
||||
**复核日期。** 上线后每周复核一次退出条数与误伤。
|
||||
|
||||
**尚未完成的两部分(属工作包三,随后补台账)。** 一是跟踪止盈策略加盘中 SAR 止损线,二是弱基本面加技术面看多的试探仓次日自动挂紧止盈(记台账 011)。
|
||||
## 011 · 2026-09-11 · 策略层的两道技术面离场:跟踪止盈盘中 SAR 止损线与弱基本面试探仓紧止盈
|
||||
|
||||
**改动一,跟踪止盈加盘中 SAR 止损线。** 自动挂载的跟踪止盈策略,每天早上把技术面映射里的 SAR 值刷进它的参数。盘中九点四十五之后,现价跌破昨日 SAR 值的千分之三缓冲就全清,当日只触发一次。这是给策略票配的趋势止损。动作引擎那条转空离场只管常规持仓,策略票的 SAR 由这道线管,两者分工不重叠。取不到 SAR 读数就撤掉旧的止损线,绝不拿旧读数当今天。
|
||||
|
||||
**改动二,弱基本面试探仓的紧止盈自动挂载。** 基本面看空加技术面看多的票,只给试探仓一批,交人拍板后入场。这类票次日早上九点四十自动挂一条跟踪止盈,回撤百分之三就全清、硬目标百分之八直接全清、带 SAR 止损线。它不占每日新挂名额,因为这是给弱基本面试探仓配的保护,不是常规的吸筹或高热挂载。认它靠入场账本里的一个标记,那是仓位矩阵给这类试探仓打的。
|
||||
|
||||
**依据。** 弱基本面的票本就是试探,一旦技术面也走坏就该快离场,所以配紧止盈加 SAR 线。SAR 是跟踪止损,翻空即趋势破位。
|
||||
|
||||
**参数。** SAR 止损线开关 PMS_TECH_SAR_STOP_ON_TRAIL 默认开,缓冲 PMS_TECH_SAR_STOP_BUFFER 默认千分之三。紧止盈回撤 PMS_TECH_TIGHT_TRAIL_GIVEBACK 默认百分之三,硬目标 PMS_TECH_TIGHT_TRAIL_TARGET 默认百分之八。
|
||||
|
||||
**预期。** 策略票在 SAR 翻空的当天被止损线保护。弱基本面试探仓入场次日就有紧止盈兜底。
|
||||
|
||||
**复核日期。** 上线后每周复核一次,看紧止盈有没有过早止损、SAR 线有没有误杀。
|
||||
|
|
|
|||
|
|
@ -83,16 +83,18 @@
|
|||
test_batch30_units.py 三源合议·工作包三离场纪律 (2026-09-11): eval_tech_exit 转空自动离场(确认清仓/
|
||||
未确认减三分之一/翻向超期不评/同一翻空一次/无读数弃权/propose_only 交人/可卖量
|
||||
夹紧/开关关掉逐字回旧)/卖出优先级目标价<研究走弱<技术面转空<保垫减仓/scan 端到端/
|
||||
策略票只看目标价按函数判(技术面转空对策略票不评)/参数登记 (18 例)
|
||||
策略票只看目标价按函数判(技术面转空对策略票不评)/跟踪止盈盘中 SAR 止损线
|
||||
(09:45 后跌破缓冲全清、当日一次、没刷进不判、开关关掉不触发)/SAR 线刷新腿只动
|
||||
自动跟踪止盈且无读数撤旧线/弱基本面试探仓紧止盈自动挂载(入场标记识别、回撤3%硬目标8%带SAR线、不占名额、冷却让路)/参数登记 (33 例)
|
||||
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
|
||||
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
|
||||
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例)
|
||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
||||
目标价到价必定入队 (档位 full 也不自动卖) +
|
||||
用户设的止损价与目标价单独成列显示 (70 例)
|
||||
共 805 例
|
||||
共 820 例
|
||||
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
||||
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例, 现为 805)
|
||||
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例, 现为 820)
|
||||
任一子集失败即整体失败 (退出码 1)。
|
||||
|
||||
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
||||
|
|
|
|||
|
|
@ -196,6 +196,221 @@ def _():
|
|||
assert abs(ps.RUNTIME_EXTRA["PMS_TECH_EXIT_TRIM_RATIO"][0] - 1.0 / 3) < 1e-3
|
||||
|
||||
|
||||
# ================================================================ F 跟踪止盈盘中 SAR 止损线 (part 3)
|
||||
def _trail_sar(price, sar_line, now, avail=6000, avg=10.0, sold_day=0):
|
||||
from app.services import strategy_runner as srun
|
||||
st = {"ts_code": "600000.SH",
|
||||
"params": {"giveback": 0.05, "sell_ratio": 1.0, "sar_line": sar_line}}
|
||||
pos = {"avg_cost": avg, "avail_qty": avail, "total_qty": avail, "cushion_pct": price / avg - 1}
|
||||
ctx = {"state": {"armed": False, "high_water": price, "sar_stop_day": sold_day}, "notes": []}
|
||||
d = srun._eval_trail(st, pos, {"price": price}, now, ctx)
|
||||
return d, ctx
|
||||
|
||||
|
||||
@case("F SAR 线·09:45 后跌破 SAR 缓冲即全清 (leg=trail_sar, 当日记一次)")
|
||||
def _():
|
||||
from datetime import datetime
|
||||
from app.services import strategy_runner as srun
|
||||
d, ctx = _trail_sar(9.9, 10.0, datetime(2026, 9, 11, 10, 0)) # 9.9 < 10×0.997=9.97
|
||||
assert d and d["action"] == srun.A_EXIT and d["leg"] == "trail_sar" and d["qty"] == 6000
|
||||
from app.core import tradedays as td
|
||||
assert ctx["state"]["sar_stop_day"] == td.ymd(datetime(2026, 9, 11, 10, 0))
|
||||
|
||||
|
||||
@case("F SAR 线·09:45 前不触发")
|
||||
def _():
|
||||
from datetime import datetime
|
||||
d, _ = _trail_sar(9.5, 10.0, datetime(2026, 9, 11, 9, 40))
|
||||
assert d is None
|
||||
|
||||
|
||||
@case("F SAR 线·现价在缓冲之上不触发")
|
||||
def _():
|
||||
from datetime import datetime
|
||||
d, _ = _trail_sar(9.98, 10.0, datetime(2026, 9, 11, 10, 0)) # 9.98 > 9.97
|
||||
assert d is None
|
||||
|
||||
|
||||
@case("F SAR 线·当日只触发一次 (已记 sar_stop_day)")
|
||||
def _():
|
||||
from datetime import datetime
|
||||
from app.core import tradedays as td
|
||||
now = datetime(2026, 9, 11, 10, 0)
|
||||
d, _ = _trail_sar(9.5, 10.0, now, sold_day=td.ymd(now))
|
||||
assert d is None
|
||||
|
||||
|
||||
@case("F SAR 线·没刷进 sar_line 时不判 (加不改, 拿不到 SAR 绝不当跌破)")
|
||||
def _():
|
||||
from datetime import datetime
|
||||
from app.services import strategy_runner as srun
|
||||
st = {"ts_code": "600000.SH", "params": {"giveback": 0.05, "sell_ratio": 1.0}} # 无 sar_line
|
||||
pos = {"avg_cost": 10.0, "avail_qty": 6000, "total_qty": 6000, "cushion_pct": -0.05}
|
||||
ctx = {"state": {"armed": False, "high_water": 9.5}, "notes": []}
|
||||
assert srun._eval_trail(st, pos, {"price": 9.5}, datetime(2026, 9, 11, 10, 0), ctx) is None
|
||||
|
||||
|
||||
@case("F SAR 线·开关关掉不触发")
|
||||
def _():
|
||||
from datetime import datetime
|
||||
saved = ps.get_bool
|
||||
ps.get_bool = lambda k, d=False: (False if k == "PMS_TECH_SAR_STOP_ON_TRAIL" else saved(k, d))
|
||||
try:
|
||||
d, _ = _trail_sar(9.5, 10.0, datetime(2026, 9, 11, 10, 0))
|
||||
assert d is None
|
||||
finally:
|
||||
ps.get_bool = saved
|
||||
|
||||
|
||||
# ================================================================ G SAR 线刷新腿 (part 3)
|
||||
@case("G 刷新腿·只动自动挂载的 ACTIVE 跟踪止盈, 网格与人工不碰")
|
||||
def _():
|
||||
from app.services import strategy_advisor as adv, tech_service
|
||||
from app.repo import pms_repo
|
||||
calls = []
|
||||
s_sm, s_up = tech_service.state_map, pms_repo.update_strategy
|
||||
tech_service.state_map = lambda: {"600000.SH": {"sar_value": 9.55}}
|
||||
pms_repo.update_strategy = lambda sid, **kw: (calls.append((sid, kw)) or 1)
|
||||
try:
|
||||
strat_all = [
|
||||
{"strategy_id": "A", "ts_code": "600000.SH", "type": "TRAIL", "status": "ACTIVE",
|
||||
"note": "自动挂载: x", "params": {"giveback": 0.05}},
|
||||
{"strategy_id": "B", "ts_code": "600000.SH", "type": "GRID", "status": "ACTIVE",
|
||||
"note": "自动挂载: x", "params": {}},
|
||||
{"strategy_id": "C", "ts_code": "600000.SH", "type": "TRAIL", "status": "ACTIVE",
|
||||
"note": "手工挂的", "params": {}},
|
||||
]
|
||||
out = {"errors": []}
|
||||
adv._refresh_sar_lines(strat_all, out)
|
||||
assert len(calls) == 1 and calls[0][0] == "A"
|
||||
assert abs(calls[0][1]["params"]["sar_line"] - 9.55) < 1e-6
|
||||
finally:
|
||||
tech_service.state_map, pms_repo.update_strategy = s_sm, s_up
|
||||
|
||||
|
||||
@case("G 刷新腿·无读数撤掉旧 SAR 线 (不拿旧读数当今天)")
|
||||
def _():
|
||||
from app.services import strategy_advisor as adv, tech_service
|
||||
from app.repo import pms_repo
|
||||
calls = []
|
||||
s_sm, s_up = tech_service.state_map, pms_repo.update_strategy
|
||||
tech_service.state_map = lambda: {} # 全无读数
|
||||
pms_repo.update_strategy = lambda sid, **kw: (calls.append((sid, kw)) or 1)
|
||||
try:
|
||||
strat_all = [{"strategy_id": "A", "ts_code": "600000.SH", "type": "TRAIL",
|
||||
"status": "ACTIVE", "note": "自动挂载: x",
|
||||
"params": {"giveback": 0.05, "sar_line": 9.9}}]
|
||||
out = {"errors": []}
|
||||
adv._refresh_sar_lines(strat_all, out)
|
||||
assert len(calls) == 1 and "sar_line" not in calls[0][1]["params"]
|
||||
finally:
|
||||
tech_service.state_map, pms_repo.update_strategy = s_sm, s_up
|
||||
|
||||
|
||||
@case("G 参数·SAR 止损线两键登记")
|
||||
def _():
|
||||
assert ps.RUNTIME_EXTRA["PMS_TECH_SAR_STOP_ON_TRAIL"][0] is True
|
||||
assert abs(ps.RUNTIME_EXTRA["PMS_TECH_SAR_STOP_BUFFER"][0] - 0.003) < 1e-9
|
||||
|
||||
|
||||
# ================================================================ H 弱基本面试探仓紧止盈自动挂载 (part 4)
|
||||
@case("H 紧止盈·入场账本 advice.tight_trail 为真才认")
|
||||
def _():
|
||||
from app.services import strategy_advisor as adv
|
||||
from app.repo import pms_repo
|
||||
s_lots, s_ins, s_led = pms_repo.list_lots, pms_repo.get_instruction, pms_repo.ledger_by_ref
|
||||
pms_repo.list_lots = lambda code, **kw: [{"instruction_id": "I1"}]
|
||||
pms_repo.get_instruction = lambda iid: {"origin_id": "P1"}
|
||||
try:
|
||||
pms_repo.ledger_by_ref = lambda refs: [{"verdict": "PASS",
|
||||
"hard_numbers": {"advice": {"tight_trail": True}}}]
|
||||
assert adv._entry_tight_trail("600000.SH") is True
|
||||
pms_repo.ledger_by_ref = lambda refs: [{"verdict": "PASS",
|
||||
"hard_numbers": {"advice": {"tight_trail": False}}}]
|
||||
assert adv._entry_tight_trail("600000.SH") is False
|
||||
pms_repo.ledger_by_ref = lambda refs: [{"verdict": "PASS", "hard_numbers": {}}]
|
||||
assert adv._entry_tight_trail("600000.SH") is False
|
||||
finally:
|
||||
pms_repo.list_lots, pms_repo.get_instruction, pms_repo.ledger_by_ref = s_lots, s_ins, s_led
|
||||
|
||||
|
||||
@case("H 紧止盈·非试探仓不挂 (返回 False, 走常规边)")
|
||||
def _():
|
||||
from app.services import strategy_advisor as adv
|
||||
s = adv._entry_tight_trail
|
||||
adv._entry_tight_trail = lambda code: False
|
||||
try:
|
||||
out = {"attached": [], "skipped": [], "blocked": []}
|
||||
r = adv._attach_tight_trail({"ts_code": "600000.SH"}, {"optout_cooldown_tdays": 10},
|
||||
set(), set(), True, out, None)
|
||||
assert r is False and not out["attached"]
|
||||
finally:
|
||||
adv._entry_tight_trail = s
|
||||
|
||||
|
||||
@case("H 紧止盈·试探仓 dry_run 挂 回撤3%/硬目标8%/带SAR线")
|
||||
def _():
|
||||
from app.services import strategy_advisor as adv, tech_service
|
||||
s_e, s_sm = adv._entry_tight_trail, tech_service.state_map
|
||||
adv._entry_tight_trail = lambda code: True
|
||||
tech_service.state_map = lambda: {"600000.SH": {"sar_value": 9.6}}
|
||||
try:
|
||||
out = {"attached": [], "skipped": [], "blocked": []}
|
||||
r = adv._attach_tight_trail({"ts_code": "600000.SH", "price": 10.0},
|
||||
{"optout_cooldown_tdays": 10}, set(), set(), True, out, None)
|
||||
assert r is True and len(out["attached"]) == 1
|
||||
pr = out["attached"][0]["params"]
|
||||
assert abs(pr["giveback"] - 0.03) < 1e-9 and abs(pr["hard_target"] - 0.08) < 1e-9
|
||||
assert abs(pr["sar_line"] - 9.6) < 1e-6
|
||||
finally:
|
||||
adv._entry_tight_trail, tech_service.state_map = s_e, s_sm
|
||||
|
||||
|
||||
@case("H 紧止盈·真挂调 strategy_service.attach 且 note 标弱基本面")
|
||||
def _():
|
||||
from app.services import strategy_advisor as adv, tech_service
|
||||
from app.repo import pms_repo
|
||||
s_e, s_sm, s_il = adv._entry_tight_trail, tech_service.state_map, pms_repo.insert_ledger
|
||||
adv._entry_tight_trail = lambda code: True
|
||||
tech_service.state_map = lambda: {}
|
||||
pms_repo.insert_ledger = lambda **kw: "L"
|
||||
calls = []
|
||||
|
||||
class SS:
|
||||
def attach(self, spec, by=None):
|
||||
calls.append(spec)
|
||||
return {"ok": True, "strategy_id": "T9"}
|
||||
try:
|
||||
out = {"attached": [], "skipped": [], "blocked": [], "errors": []}
|
||||
r = adv._attach_tight_trail({"ts_code": "600000.SH", "price": 10.0},
|
||||
{"optout_cooldown_tdays": 10}, set(), set(), False, out, SS())
|
||||
assert r is True and calls and calls[0]["type"] == "TRAIL"
|
||||
assert "试探仓" in calls[0]["note"] and "紧止盈" in calls[0]["note"]
|
||||
assert out["attached"][0]["strategy_id"] == "T9"
|
||||
finally:
|
||||
adv._entry_tight_trail, tech_service.state_map, pms_repo.insert_ledger = s_e, s_sm, s_il
|
||||
|
||||
|
||||
@case("H 紧止盈·人工撤下冷却期内占位让路 (返回 True 不走常规边, 不挂)")
|
||||
def _():
|
||||
from app.services import strategy_advisor as adv
|
||||
s_e = adv._entry_tight_trail
|
||||
adv._entry_tight_trail = lambda code: True
|
||||
try:
|
||||
out = {"attached": [], "skipped": [], "blocked": []}
|
||||
r = adv._attach_tight_trail({"ts_code": "600000.SH"}, {"optout_cooldown_tdays": 10},
|
||||
set(), {("600000.SH", adv.R_TRAIL)}, True, out, None)
|
||||
assert r is True and len(out["skipped"]) == 1 and not out["attached"]
|
||||
finally:
|
||||
adv._entry_tight_trail = s_e
|
||||
|
||||
|
||||
@case("H 参数·紧止盈两键登记")
|
||||
def _():
|
||||
assert abs(ps.RUNTIME_EXTRA["PMS_TECH_TIGHT_TRAIL_GIVEBACK"][0] - 0.03) < 1e-9
|
||||
assert abs(ps.RUNTIME_EXTRA["PMS_TECH_TIGHT_TRAIL_TARGET"][0] - 0.08) < 1e-9
|
||||
|
||||
|
||||
def main():
|
||||
ok = 0
|
||||
for name, fn in RESULTS:
|
||||
|
|
|
|||
Loading…
Reference in New Issue