添加交易逻辑
This commit is contained in:
parent
43ccc9bf0f
commit
e03e41a32b
|
|
@ -50,6 +50,9 @@ app/services/strategy_advisor.py(新增);app/services/strategy_service.py
|
|||
**真机判收**
|
||||
部分判收(2026-08-25 盘中)。桥机 make deploy + make test 见 ALL SUITES PASS;dry-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_lot(688/689 按 200 股),网格手工挂的 100 股档在评估时抬成合法数量、卖出量不足 200 股原地等不推进档位;跟踪止盈的部分卖低于 200 股时量够就抬到 200(方向是保利润,偏保守)、可卖本身不足 200 时部分卖放弃,全清路径按交易所例外允许不足 200 股一次性全卖。主板行为一字未变,单测把两侧都钉住(批十七扩到 38 例,总数 557)。做T是命令授权的手工策略,科创板做T的开仓与平回数量怎么处理需要单独拍板,本次不动。
|
||||
二,管理页「我的策略」加了「自动挂载 · 看看今天想挂什么」按钮:调 09:40 调度位同一份实现的试算模式,把想挂、想换挂、想停买入、被挡、跳过的每一条连原因渲染成可读文字,不再需要敲命令;空结果明说「没有想动的是正常克制」。
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue