添加交易逻辑

This commit is contained in:
zlt 2026-08-26 10:01:30 +08:00
parent 43ccc9bf0f
commit e03e41a32b
5 changed files with 106 additions and 6 deletions

View File

@ -50,6 +50,9 @@ app/services/strategy_advisor.py新增app/services/strategy_service.py
**真机判收**
部分判收2026-08-25 盘中)。桥机 make deploy + make test 见 ALL SUITES PASSdry-run 试算真机四票读数全部合理688802.SH 想挂网格但「按投入比例折出 47,250 元买不起一手」被挡科创板高价票判定正确002179.SZ 有在途指令主动让路其余两票无信号静默跳过unknown_states 为空(子串归类修对了)。**尚未判收的**:真挂一条(等在途清了重扫或次日 09:40、边三/边四真机走一遍、strategy_runner 对自动网格的逐档发单。
**次日上午第四批2026-08-26 盘中):每票阶段可视化**
用户拍板把状态机补成看得见的:扫描结果新增 stages对**每只**持仓票各出一行——处在五阶段(吸筹震荡、启动拉升、高位派发、深亏修复、中性)的哪一段、一句话理由、热度、浮盈、定性原文;已挂自动策略的票阶段以策略为准(网格即吸筹震荡进行中、止盈即启动拉升保护中),人工策略票按信号判并注明「自动挂载不碰」;信号判定次序与连边一致,派发优先于深亏(保守方向)。页面按钮的结果顶部渲染五阶段视图,不动的票也说清为什么不动。判定函数 stage_of 纯逻辑可测,批十七扩到 40 例,总数 559。同日运维记录桥的计划触发已在北京 07:10 跑通(计划日 T-1判收通过deploy 恰好压掉 09:40 调度位,用试算端点的正式模式手动补跑(补跑与调度位同一份实现);清掉两个三周前 compose run 未带 --rm 的残留容器。
**当日盘中第三批(赶进度拍板后加的两件)**
科创板取整从「挂载侧兜」补到「执行侧真修」strategy_runner 新增 _min_lot688/689 按 200 股),网格手工挂的 100 股档在评估时抬成合法数量、卖出量不足 200 股原地等不推进档位;跟踪止盈的部分卖低于 200 股时量够就抬到 200方向是保利润偏保守、可卖本身不足 200 时部分卖放弃,全清路径按交易所例外允许不足 200 股一次性全卖。主板行为一字未变,单测把两侧都钉住(批十七扩到 38 例,总数 557。做T是命令授权的手工策略科创板做T的开仓与平回数量怎么处理需要单独拍板本次不动。
二,管理页「我的策略」加了「自动挂载 · 看看今天想挂什么」按钮:调 09:40 调度位同一份实现的试算模式,把想挂、想换挂、想停买入、被挡、跳过的每一条连原因渲染成可读文字,不再需要敲命令;空结果明说「没有想动的是正常克制」。

View File

@ -62,6 +62,10 @@ NOTE_AUTO = "自动挂载: "
NOTE_HANDOFF = "自动挂载(接力): "
MARK_HANDOFF_OUT = "[接力撤下]"
# 阶段名 (方案第三节五阶段; 页面显示与判分同用这份字面量)
STG_ACCUM, STG_LAUNCH = "吸筹震荡", "启动拉升"
STG_DISTRIB, STG_REPAIR, STG_NEUTRAL = "高位派发", "深亏修复", "中性"
ACCUM_WINDOW_DAYS = 45 # 每票取近 45 自然日内最新一条结论
HEAT_MAX_AGE_DAYS = 4 # 热度表末日落后超此自然日 → 热度信号本轮不可用
_RULE_OF_TYPE = {"GRID": R_GRID, "TRAIL": R_TRAIL}
@ -151,6 +155,30 @@ def plan_edge(*, cls, fresh, heat, cushion, prm):
return None, ""
def stage_of(*, cls, fresh, heat, heat_th, cushion, stype=None, is_auto=False):
"""判定一只持仓票当前处在哪个阶段 (方案第三节的五阶段)。返回 (阶段, 一句话理由)。
已挂自动策略的票, 阶段以策略为准 策略本身就是阶段判断落了地无策略的票按
信号判, 次序与连边判断一致, 保守方向优先: 派发最先, 然后吸筹拉升深亏,
不是就是中性这个函数只读不写, 每次扫描对全部持仓各出一行, 页面按钮直接显示
"""
t = str(stype or "").upper()
if is_auto and t == "GRID":
return STG_ACCUM, "自动网格进行中, 区间内低买高卖"
if is_auto and t == "TRAIL":
return STG_LAUNCH, "自动跟踪止盈保护中, 创新高抬线回落即卖"
if cls == CLS_DISTRIB:
return STG_DISTRIB, "决策系统定性为高位派发"
if cls == CLS_CLEAR and fresh:
return STG_ACCUM, "明确吸筹且结论新鲜"
hot = heat is not None and _f(heat, 0.0) >= heat_th
if hot and cushion is not None and _f(cushion, 0.0) > 0:
return STG_LAUNCH, f"热度 {_f(heat):.2f} 达标且有浮盈"
if cushion is not None and _f(cushion, 0.0) < 0:
return STG_REPAIR, "安全垫为负, 交给补仓评估与清弱票, 不配策略"
return STG_NEUTRAL, "无明确信号, 留给动作引擎的常规动作"
def count_auto_today(rows, today_ymd: int) -> int:
"""今天已常规自动挂载几条 (接力挂出的不算 —— 它是换不是增)。rows=策略行 (含已归档)。"""
n = 0
@ -340,7 +368,7 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
out = {"ok": True, "enabled": prm["enabled"], "dry_run": dry_run, "checked": 0,
"attached": [], "handoffs": [], "paused": [], "resumed": [],
"blocked": [], "skipped": [], "errors": [],
"unknown_states": []}
"unknown_states": [], "stages": []}
if not prm["enabled"]:
out["skipped"].append({"why": "策略自动挂载总开关关闭 (PMS_AUTO_STRATEGY_ENABLED)"})
return out
@ -438,6 +466,16 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
and a["age_days"] <= prm["accum_stale_tdays"] * 2)
hv = heat.get(code)
st = strat_by_code.get(code)
is_auto = bool(st and str(st.get("note") or "").startswith("自动挂载"))
stg, stg_why = stage_of(cls=a.get("cls"), fresh=fresh, heat=hv,
heat_th=prm["heat_th"], cushion=p.get("cushion_pct"),
stype=(st or {}).get("type"), is_auto=is_auto)
if st and not is_auto:
stg_why += " (挂着人工策略, 自动挂载不碰)"
out["stages"].append({"ts_code": code, "stage": stg, "why": stg_why,
"accum_state": a.get("state"), "heat": hv,
"cushion_pct": p.get("cushion_pct"),
"strategy": (st or {}).get("type")})
if st:
_tend_existing(st, p, a, fresh, hv, prm, buypause, handoff_cool,
today, dry_run, out, strategy_service)

View File

@ -665,6 +665,17 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
试算只判断、不挂载不留痕;真跑在每个交易日 09:40 由调度自动执行。本轮查了
<b>{{ autoScanRes.checked }}</b> 只持仓票。
</div>
<!-- 五阶段视图: 每只持仓票当前处在打法状态机的哪个阶段, 不动的票也说清为什么不动 -->
<div v-for="(x,i) in (autoScanRes.stages||[])" :key="'stg'+i" style="margin:2px 0">
<el-tag size="small" effect="dark" :type="stageTagType(x.stage)">{{ x.stage }}</el-tag>
<b> {{ nm(x.ts_code) }}</b>
<span class="muted"> {{ x.why }}
<template v-if="x.heat!=null"> · 热度 {{ Number(x.heat).toFixed(2) }}</template>
<template v-if="x.cushion_pct!=null"> · 浮盈 {{ pct(x.cushion_pct) }}</template>
<template v-if="x.accum_state"> · 定性「{{ x.accum_state }}」</template>
</span>
</div>
<el-divider v-if="(autoScanRes.stages||[]).length" style="margin:6px 0"></el-divider>
<div v-for="(x,i) in (autoScanRes.attached||[])" :key="'aa'+i">
<el-tag size="small" type="success" effect="dark">想挂</el-tag>
<b> {{ nm(x.ts_code) }}</b> {{ tx('stype', x.type) }} —— {{ x.why }}</div>
@ -2382,6 +2393,11 @@ createApp({
strategies.value = d.strategies || (d.data && d.data.strategies) || [];
stratEnabled.value = !!(d.enabled != null ? d.enabled : (d.data && d.data.enabled));
}
// 五阶段标签配色 (阶段字面量与 strategy_advisor.stage_of 是同一份契约)
function stageTagType(s) {
return { '吸筹震荡': 'primary', '启动拉升': 'success', '高位派发': 'danger',
'深亏修复': 'warning', '中性': 'info' }[s] || 'info';
}
// 自动挂载试算: 只判断不落任何东西 (与 09:40 调度位同一份实现, 带 dry_run)
async function autoScanPreview() {
autoScanBusy.value = true;
@ -2544,7 +2560,7 @@ createApp({
pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit,
pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy,
strategies, stratEnabled, opLog, stratDlg, stratQty, loadStrategies, loadOpLog,
autoScanRes, autoScanBusy, autoScanPreview,
autoScanRes, autoScanBusy, autoScanPreview, stageTagType,
openStrategy, validateStrategy, attachStrategy, setStrategyStatus, stratStateText, resumeBuy,
showLedger, onPosExpand,
openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, loadOpenScan, denyToday,

View File

@ -28,11 +28,11 @@
test_batch16_units.py 软归档 archived_at: 单表守卫/只归终态/列表默认排除 (5 )
test_batch17_units.py 策略自动挂载: 定性归类(子串+保守优先)/网格参数生成/
科创板一手200(advisor runner 两侧)/连边矩阵/
note 约定与冷却推导/接力判定/clear_buypause/
编排冒烟(dry_run 滴水不写/名额/暂停买入/接力全链)/
判分脚本聚合与对照分组 (38 )
五阶段判定与逐票阶段返回/note 约定与冷却推导/
接力判定/clear_buypause/编排冒烟(dry_run 滴水不写/
名额/暂停买入/接力全链)/判分脚本聚合与对照分组 (40 )
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) (58 )
557
559
任一子集失败即整体失败 (退出码 1)
"""
import os

View File

@ -679,6 +679,49 @@ def _():
param_store.get_bool, adv._params = orig, orig_p
@case("[阶段] stage_of 五阶段判定矩阵: 策略优先于信号, 派发优先于深亏, 都不是归中性")
def _():
def s(**kw):
d = dict(cls=None, fresh=False, heat=None, heat_th=0.80, cushion=None,
stype=None, is_auto=False)
d.update(kw)
return adv.stage_of(**d)[0]
# 已挂自动策略: 阶段以策略为准, 信号什么样都不改判
assert s(stype="GRID", is_auto=True, cls=adv.CLS_DISTRIB) == adv.STG_ACCUM
assert s(stype="TRAIL", is_auto=True, cushion=-0.1) == adv.STG_LAUNCH
# 人工策略不算数 (is_auto=False), 按信号判
assert s(stype="GRID", is_auto=False, cls=adv.CLS_DISTRIB) == adv.STG_DISTRIB
# 信号判定次序: 派发 > 吸筹 > 拉升 > 深亏 > 中性
assert s(cls=adv.CLS_DISTRIB, cushion=-0.2) == adv.STG_DISTRIB
assert s(cls=adv.CLS_CLEAR, fresh=True, cushion=-0.05) == adv.STG_ACCUM
assert s(cls=adv.CLS_CLEAR, fresh=False, cushion=-0.05) == adv.STG_REPAIR
assert s(heat=0.9, cushion=0.03) == adv.STG_LAUNCH
assert s(heat=0.9, cushion=-0.03) == adv.STG_REPAIR # 热但亏, 不算拉升
assert s(heat=0.5, cushion=0.03) == adv.STG_NEUTRAL
assert s() == adv.STG_NEUTRAL
@case("[阶段] 扫描给每只持仓各出一行阶段, 人工策略票注明不碰")
def _():
held = [_pos(ts_code="600000.SH"), # 明确吸筹 → 吸筹震荡
_pos(ts_code="600519.SH", cushion_pct=-0.06), # 深亏修复
_pos(ts_code="000001.SZ"), # 无信号 → 中性
_pos(ts_code="002415.SZ")] # 人工策略
accum = {"600000.SH": {"cls": adv.CLS_CLEAR, "state": "明确吸筹", "score": 80,
"ymd": TODAY, "age_days": 1}}
manual = [{"strategy_id": "S_M", "ts_code": "002415.SZ", "type": "T0",
"note": "手工做T", "params": {}, "state": {}}]
rec = _Rec()
out = _scan_stubbed(rec, held=held, accum=accum, heat={}, strategies=manual,
dry_run=True)
stg = {x["ts_code"]: x for x in out["stages"]}
assert len(out["stages"]) == 4, out["stages"]
assert stg["600000.SH"]["stage"] == adv.STG_ACCUM
assert stg["600519.SH"]["stage"] == adv.STG_REPAIR
assert stg["000001.SZ"]["stage"] == adv.STG_NEUTRAL
assert "人工策略" in stg["002415.SZ"]["why"], stg["002415.SZ"]
@case("[科创板] runner 止盈卖出: 200 股起, 不足 200 只许一次性全清, 主板行为一字不变")
def _():
from app.services import strategy_runner as srun