添加交易逻辑
This commit is contained in:
parent
f4123d36d3
commit
e4c23ec238
|
|
@ -48,10 +48,13 @@ app/services/strategy_advisor.py(新增);app/services/strategy_service.py
|
||||||
桥机 factorevaluation 上收盘后 make deploy(源码打进镜像),make test 见 ALL SUITES PASS。不动 .env,不需要迁移。上线即全自动(拍板③),随时可在参数页把 PMS_AUTO_STRATEGY_ENABLED 或单独把接力 PMS_AUTO_HANDOFF_ENABLED 关掉,即时生效。
|
桥机 factorevaluation 上收盘后 make deploy(源码打进镜像),make test 见 ALL SUITES PASS。不动 .env,不需要迁移。上线即全自动(拍板③),随时可在参数页把 PMS_AUTO_STRATEGY_ENABLED 或单独把接力 PMS_AUTO_HANDOFF_ENABLED 关掉,即时生效。
|
||||||
|
|
||||||
**真机判收**
|
**真机判收**
|
||||||
未判收。开发容器全量单测 ALL SUITES PASS(553 例)。判收建议:部署当天先 curl -X POST 'http://127.0.0.1:38100/api/ops/strategy-attach-scan?dry_run=true' 看它想挂什么、排除原因对不对(尤其 unknown_states 是否为空);次日 09:40 后看 beat 日志 strategy_attach 一跳的 attached/handoffs/paused,页面策略列表出现「自动挂载: 」开头的条目、台账出现 ATTACH 留痕;挂出的网格由 strategy_runner 正常逐档发单、每笔过闸。
|
部分判收(2026-08-25 盘中)。桥机 make deploy + make test 见 ALL SUITES PASS;dry-run 试算真机四票读数全部合理:688802.SH 想挂网格但「按投入比例折出 47,250 元买不起一手」被挡(科创板高价票,判定正确);002179.SZ 有在途指令主动让路;其余两票无信号静默跳过;unknown_states 为空(子串归类修对了)。**尚未判收的**:真挂一条(等在途清了重扫或次日 09:40)、边三/边四真机走一遍、strategy_runner 对自动网格的逐档发单。
|
||||||
|
|
||||||
|
**当日盘中补两处(第二次交付)**
|
||||||
|
一,科创板一手口径:dry-run 里 688802.SH 暴露全库按 100 股一手,而 688/689 最小申报 200 股——自动网格若生成 per_lot=100 会被券商拒单。advisor 补 lot_of()(688/689→200),买得起一手与 per_lot 下限都按它算(200 仍是 100 的整数倍,runner 的取整不会磨掉)。runner 侧卖出腿对科创板的整百取整仍不完美,见欠账。二,判分读数脚本 scripts/report_strategy_score.py 交付:四段只读统计(样本盘点 / 网格差价按策略内均价配对 / 止盈「卖点之后又跌多少=保住的钱」/ 挂上 vs 名额挡下的对照组涨跌),样本不足三条只报数不下结论,连不上库或取不到现价都说人话。batch17 扩到 36 例,总数 555。
|
||||||
|
|
||||||
**还欠着什么**
|
**还欠着什么**
|
||||||
一,判分闭环的读数脚本(report_strategy_score.py:自动挂载 vs 人工挂载 vs 不挂的对照统计)本次未写——留痕字段(ATTACH/HANDOFF/NOTE 台账 + note 约定)已齐,脚本只读统计,等跑出两周数据再交,先欠着。二,探测顺带发现候选池计划日停在 2026-08-21(探测日 08-25,日龄已 2 个交易日以上),疑似上游桥的日更链又停了,且 PMS_PLAN_STALE_TDAYS 可能被调宽过——与本特性无关,单独排查。三,热度阈值 0.80 与日上限 2 都是首版拍的,判分数据出来后用读数回调。
|
一,判分脚本已交,但**有意义的读数要等两周样本**——现在跑是空跑冒烟,别拿首周数字调阈值(热度 0.80 / 日上限 2 等数据回调)。二,探测顺带发现候选池计划日停在 2026-08-21(探测日 08-25,日龄已 2 个交易日以上),疑似上游桥的日更链又停了,且 PMS_PLAN_STALE_TDAYS 可能被调宽过——与本特性无关,单独排查。三,科创板卖出腿:strategy_runner 的 _round_lot 全库按 100 股取整,688 票的止盈卖出若折出 100 股会被拒单(低于 200 股只能一次性清)——影响面小(当前仅一只 688 且它连网格都挂不上),排查后单独修。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,18 @@ HEAT_MAX_AGE_DAYS = 4 # 热度表末日落后超此自然日 → 热
|
||||||
_RULE_OF_TYPE = {"GRID": R_GRID, "TRAIL": R_TRAIL}
|
_RULE_OF_TYPE = {"GRID": R_GRID, "TRAIL": R_TRAIL}
|
||||||
|
|
||||||
|
|
||||||
|
def lot_of(ts_code) -> int:
|
||||||
|
"""最小申报单位: 科创板 (688/689) 200 股起, 其余 100。
|
||||||
|
|
||||||
|
2026-08-25 实盘 dry-run 发现: 持仓里有 688802.SH, 全库其他地方一律按 100 股一手
|
||||||
|
(strategy_runner._round_lot / sizer), 科创板 200 股起买这条只在这里兜 —— 自动网格
|
||||||
|
的 per_lot 若生成 100, runner 发单会被券商按无效数量拒掉。买得起一手的判断与
|
||||||
|
per_lot 下限都按这个数; 200 也是 100 的整数倍, 不会被 _round_lot 磨掉。
|
||||||
|
runner 侧卖出腿的整百取整对科创板仍不完美 (见 DEVLOG 欠账), 不在本模块修。"""
|
||||||
|
s = str(ts_code or "")
|
||||||
|
return 200 if s.startswith(("688", "689")) else 100
|
||||||
|
|
||||||
|
|
||||||
def _f(v, d=None):
|
def _f(v, d=None):
|
||||||
try:
|
try:
|
||||||
return float(v)
|
return float(v)
|
||||||
|
|
@ -86,19 +98,22 @@ def classify_accum(state) -> str:
|
||||||
return CLS_UNKNOWN
|
return CLS_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
def grid_params(*, price, support, pressure, band, step_pct, cap_room, cap_ratio):
|
def grid_params(*, price, support, pressure, band, step_pct, cap_room, cap_ratio,
|
||||||
|
lot=100):
|
||||||
"""网格参数自动生成 (方案附录二)。返回 (params, why); params=None 时 why 说明放弃原因。
|
"""网格参数自动生成 (方案附录二)。返回 (params, why); params=None 时 why 说明放弃原因。
|
||||||
|
|
||||||
区间优先锚支撑压力, 锚不住退百分比带; 任何一步不满足 0<下界<中枢<上界 就放弃不硬凑。
|
区间优先锚支撑压力, 锚不住退百分比带; 任何一步不满足 0<下界<中枢<上界 就放弃不硬凑。
|
||||||
|
lot=最小申报单位 (科创板 200, 见 lot_of) —— 买得起一手与 per_lot 下限都按它算。
|
||||||
"""
|
"""
|
||||||
p = _f(price, 0.0)
|
p = _f(price, 0.0)
|
||||||
if not p or p <= 0:
|
if not p or p <= 0:
|
||||||
return None, "取不到实时价, 网格区间无从定"
|
return None, "取不到实时价, 网格区间无从定"
|
||||||
|
lot = int(lot or 100)
|
||||||
cap = _f(cap_room, 0.0) or 0.0
|
cap = _f(cap_room, 0.0) or 0.0
|
||||||
max_capital = round(cap * _f(cap_ratio, 0.5), 2)
|
max_capital = round(cap * _f(cap_ratio, 0.5), 2)
|
||||||
if max_capital < p * 100:
|
if max_capital < p * lot:
|
||||||
return None, (f"单股上限余量 {cap:,.0f} 元按投入比例折出 {max_capital:,.0f} 元, "
|
return None, (f"单股上限余量 {cap:,.0f} 元按投入比例折出 {max_capital:,.0f} 元, "
|
||||||
f"买不起一手, 不挂")
|
f"买不起一手({lot}股), 不挂")
|
||||||
r, s = _f(pressure, 0.0) or 0.0, _f(support, 0.0) or 0.0
|
r, s = _f(pressure, 0.0) or 0.0, _f(support, 0.0) or 0.0
|
||||||
upper = round(r * 1.01, 3) if r > p else round(p * (1 + band), 3)
|
upper = round(r * 1.01, 3) if r > p else round(p * (1 + band), 3)
|
||||||
lower = round(s * 0.99, 3) if 0 < s < p else round(p * (1 - band), 3)
|
lower = round(s * 0.99, 3) if 0 < s < p else round(p * (1 - band), 3)
|
||||||
|
|
@ -108,8 +123,8 @@ def grid_params(*, price, support, pressure, band, step_pct, cap_room, cap_ratio
|
||||||
step = max(0.005, _f(step_pct, 0.02))
|
step = max(0.005, _f(step_pct, 0.02))
|
||||||
n_below = max(1, int((p - lower) / (p * step)))
|
n_below = max(1, int((p - lower) / (p * step)))
|
||||||
per_lot = int(max_capital / n_below / p / 100) * 100
|
per_lot = int(max_capital / n_below / p / 100) * 100
|
||||||
if per_lot < 100:
|
if per_lot < lot:
|
||||||
per_lot = 100
|
per_lot = lot
|
||||||
return ({"center": p, "lower": lower, "upper": upper, "step_pct": step,
|
return ({"center": p, "lower": lower, "upper": upper, "step_pct": step,
|
||||||
"per_lot": per_lot, "max_capital": max_capital}, "")
|
"per_lot": per_lot, "max_capital": max_capital}, "")
|
||||||
|
|
||||||
|
|
@ -544,7 +559,8 @@ def _attach_one(edge, why, p, a, hv, prm, view, black, live_codes, optout,
|
||||||
gp, gwhy = grid_params(price=(p.get("price") if p.get("price_ok") else None),
|
gp, gwhy = grid_params(price=(p.get("price") if p.get("price_ok") else None),
|
||||||
support=p.get("support_ref"), pressure=p.get("pressure_ref"),
|
support=p.get("support_ref"), pressure=p.get("pressure_ref"),
|
||||||
band=prm["grid_band"], step_pct=prm["grid_step_pct"],
|
band=prm["grid_band"], step_pct=prm["grid_step_pct"],
|
||||||
cap_room=cap_room, cap_ratio=prm["grid_cap_ratio"])
|
cap_room=cap_room, cap_ratio=prm["grid_cap_ratio"],
|
||||||
|
lot=lot_of(code))
|
||||||
if not gp:
|
if not gp:
|
||||||
out["blocked"].append({"ts_code": code, "edge": edge, "why": gwhy})
|
out["blocked"].append({"ts_code": code, "edge": edge, "why": gwhy})
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
策略判分读数 · 只读统计 (STRATEGY_AUTO_ATTACH_PLAN.md 判分闭环, 拍板⑤)
|
||||||
|
======================================================================
|
||||||
|
只读, 不写任何表, 随时可跑。回答一个问题: **自动挂载的策略到底有没有挣到钱**。
|
||||||
|
三张对照 + 一张盘点:
|
||||||
|
|
||||||
|
零. 样本盘点 自动策略共几条 (在跑/撤下/接力), 台账留痕几条 —— 先看样本够不够
|
||||||
|
一. 网格差价 每条自动网格: 买了多少/卖了多少/已实现差价/在手浮动
|
||||||
|
二. 止盈判分 每条自动止盈: 触发过没有; 触发的 = 卖点之后又跌了多少 (保住的钱,
|
||||||
|
负数=卖飞); 没触发的 = 高水位与武装状态
|
||||||
|
三. 挂/未挂对照 窗口内 ATTACH 成功的票 vs 想挂但被**每日名额**挡下的票,
|
||||||
|
各自从决定日到现在的涨跌 —— 名额挡下的是天然对照组
|
||||||
|
(资格全同, 只差没轮上); 排除项挡下的 (冻结/买不起/黑名单)
|
||||||
|
结构上就不同, 不进对照。
|
||||||
|
|
||||||
|
运行 (桥机 factorevaluation):
|
||||||
|
docker compose run --rm pms-web python scripts/report_strategy_score.py [--days 30]
|
||||||
|
|
||||||
|
读数纪律:
|
||||||
|
- 差价用**策略内均价配对** (卖出收益 = 卖出额 − 卖出股数×买入均价), 不做逐笔 FIFO ——
|
||||||
|
月度读数要的是方向和量级, 不是会计账; 逐笔账在 pms_lot 里, 要精算去查它。
|
||||||
|
- 现价来自分钟线缓存 (market.get_prices), 收盘后或缓存过期时取不到 → 相应栏目
|
||||||
|
明说「无现价」, 绝不拿旧价冒充。
|
||||||
|
- 样本少于三条的段落只列数不下结论 —— 判分要等数据, 不等数据的判分是编故事。
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from app.db.session import fetch_all # noqa: E402
|
||||||
|
from app.repo import pms_repo # noqa: E402
|
||||||
|
from app.services.strategy_advisor import ( # noqa: E402
|
||||||
|
NOTE_AUTO, NOTE_HANDOFF, MARK_HANDOFF_OUT)
|
||||||
|
|
||||||
|
MIN_SAMPLE = 3 # 少于这个数只报数不下结论
|
||||||
|
|
||||||
|
|
||||||
|
def _f(v, d=0.0):
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _pct(a, b):
|
||||||
|
"""b 相对 a 的涨跌; 任一边取不到返回 None。"""
|
||||||
|
a, b = _f(a), _f(b)
|
||||||
|
return (b / a - 1.0) if a > 0 and b > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_pct(x):
|
||||||
|
return f"{x:+.2%}" if x is not None else "—"
|
||||||
|
|
||||||
|
|
||||||
|
def auto_strategies():
|
||||||
|
"""全部自动策略 (含已归档已撤销), 按 note 前缀识别。"""
|
||||||
|
rows = pms_repo.list_strategies(limit=1000, include_archived=True)
|
||||||
|
return [r for r in rows if str(r.get("note") or "").startswith("自动挂载")]
|
||||||
|
|
||||||
|
|
||||||
|
def fills_of(strategy_id):
|
||||||
|
"""该策略名下有成交的指令 (exec_qty>0, 不论终态) → 买卖两侧合计。单表。"""
|
||||||
|
rows = fetch_all(
|
||||||
|
"SELECT side, exec_qty, exec_avg_price FROM pms_instruction "
|
||||||
|
"WHERE origin_type = 'strategy' AND origin_id = :sid AND exec_qty > 0",
|
||||||
|
{"sid": strategy_id})
|
||||||
|
agg = {"buy": {"qty": 0, "amt": 0.0, "n": 0}, "sell": {"qty": 0, "amt": 0.0, "n": 0}}
|
||||||
|
for r in rows:
|
||||||
|
side = str(r.get("side") or "").lower()
|
||||||
|
if side not in agg:
|
||||||
|
continue
|
||||||
|
q, px = int(r.get("exec_qty") or 0), _f(r.get("exec_avg_price"))
|
||||||
|
agg[side]["qty"] += q
|
||||||
|
agg[side]["amt"] += q * px
|
||||||
|
agg[side]["n"] += 1
|
||||||
|
return agg
|
||||||
|
|
||||||
|
|
||||||
|
def ledger_window(days):
|
||||||
|
"""窗口内 advisor 留的 ATTACH 痕 (PASS=挂了 / NOTE+名额已满=对照)。单表。"""
|
||||||
|
since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
rows = fetch_all(
|
||||||
|
"SELECT ts_code, decided_at, verdict, price_at, reason, ref_id "
|
||||||
|
"FROM pms_action_ledger WHERE action = 'ATTACH' AND decided_at >= :since",
|
||||||
|
{"since": since})
|
||||||
|
attached, control = [], []
|
||||||
|
for r in rows:
|
||||||
|
if str(r.get("verdict")) == "PASS":
|
||||||
|
attached.append(r)
|
||||||
|
elif "名额已满" in str(r.get("reason") or ""):
|
||||||
|
control.append(r)
|
||||||
|
return attached, control
|
||||||
|
|
||||||
|
|
||||||
|
def current_prices(codes):
|
||||||
|
try:
|
||||||
|
from app.services import market
|
||||||
|
return market.get_prices(sorted(set(c for c in codes if c)))
|
||||||
|
except Exception as e: # noqa: BLE001 —— redis 不在 (本地跑) 也要能出报告
|
||||||
|
print(f" (现价不可用: {type(e).__name__}: {e} —— 浮动与保住栏按无现价处理)")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--days", type=int, default=30, help="对照窗口 (自然日, 默认 30)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
print("=" * 66)
|
||||||
|
print("策略判分读数 %s (只读; 窗口 %d 天)"
|
||||||
|
% (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), args.days))
|
||||||
|
print("=" * 66)
|
||||||
|
|
||||||
|
try:
|
||||||
|
strats = auto_strategies()
|
||||||
|
except Exception as e: # noqa: BLE001 —— 连不上库也要说人话, 不甩栈
|
||||||
|
print(f"\n✗ 策略表读取失败: {type(e).__name__}: {e}\n"
|
||||||
|
f" (本脚本要在桥机容器里跑: docker compose run --rm pms-web "
|
||||||
|
f"python scripts/report_strategy_score.py)")
|
||||||
|
sys.exit(1)
|
||||||
|
grids = [s for s in strats if str(s.get("type")).upper() == "GRID"]
|
||||||
|
trails = [s for s in strats if str(s.get("type")).upper() == "TRAIL"]
|
||||||
|
handoff_in = [s for s in strats if str(s.get("note") or "").startswith(NOTE_HANDOFF)]
|
||||||
|
handoff_out = [s for s in strats if MARK_HANDOFF_OUT in str(s.get("note") or "")]
|
||||||
|
codes = {s.get("ts_code") for s in strats}
|
||||||
|
|
||||||
|
# ---- 零、样本盘点 ----
|
||||||
|
print("\n【零】样本盘点")
|
||||||
|
if not strats:
|
||||||
|
print(" 还没有任何自动挂载的策略 —— 判分从第一条挂出才开始, 本报告先当空跑冒烟。")
|
||||||
|
else:
|
||||||
|
by_status = {}
|
||||||
|
for s in strats:
|
||||||
|
by_status[s.get("status")] = by_status.get(s.get("status"), 0) + 1
|
||||||
|
print(f" 自动策略共 {len(strats)} 条 (网格 {len(grids)} / 止盈 {len(trails)}), "
|
||||||
|
f"状态: " + ", ".join(f"{k} {v}" for k, v in sorted(by_status.items())))
|
||||||
|
print(f" 接力: 换挂出的止盈 {len(handoff_in)} 条, 被接力撤下的网格 {len(handoff_out)} 条")
|
||||||
|
px = current_prices(codes) if strats else {}
|
||||||
|
|
||||||
|
# ---- 一、网格差价 ----
|
||||||
|
print("\n【一】网格差价 (含已撤下的; 差价=卖出额−卖出股数×买入均价, 均价配对口径)")
|
||||||
|
if not grids:
|
||||||
|
print(" 无样本。")
|
||||||
|
tot_real, tot_float, n_traded = 0.0, 0.0, 0
|
||||||
|
for s in grids:
|
||||||
|
agg = fills_of(s["strategy_id"])
|
||||||
|
b, sl = agg["buy"], agg["sell"]
|
||||||
|
code, sid = s.get("ts_code"), s["strategy_id"]
|
||||||
|
if b["qty"] == 0 and sl["qty"] == 0:
|
||||||
|
print(f" {code} {sid} [{s.get('status')}]: 还没成交过一笔")
|
||||||
|
continue
|
||||||
|
n_traded += 1
|
||||||
|
buy_avg = b["amt"] / b["qty"] if b["qty"] else 0.0
|
||||||
|
realized = sl["amt"] - sl["qty"] * buy_avg if sl["qty"] else 0.0
|
||||||
|
net_qty = b["qty"] - sl["qty"]
|
||||||
|
cur = _f(px.get(code))
|
||||||
|
floating = net_qty * (cur - buy_avg) if (net_qty > 0 and cur > 0 and buy_avg > 0) else None
|
||||||
|
tot_real += realized
|
||||||
|
if floating is not None:
|
||||||
|
tot_float += floating
|
||||||
|
print(f" {code} {sid} [{s.get('status')}]: "
|
||||||
|
f"买 {b['qty']} 股/{b['amt']:,.0f} 元 (均 {buy_avg:.3f}), "
|
||||||
|
f"卖 {sl['qty']} 股/{sl['amt']:,.0f} 元, 已实现 {realized:+,.0f} 元, "
|
||||||
|
+ (f"在手 {net_qty} 股浮动 {floating:+,.0f} 元" if floating is not None
|
||||||
|
else f"在手 {net_qty} 股 (无现价, 浮动不算)"))
|
||||||
|
if grids:
|
||||||
|
print(f" 合计: 有成交的 {n_traded}/{len(grids)} 条, "
|
||||||
|
f"已实现 {tot_real:+,.0f} 元, 可算浮动 {tot_float:+,.0f} 元"
|
||||||
|
+ ("" if n_traded >= MIN_SAMPLE else f" (样本不足 {MIN_SAMPLE} 条, 只报数不下结论)"))
|
||||||
|
|
||||||
|
# ---- 二、止盈判分 ----
|
||||||
|
print("\n【二】止盈判分 (触发的看「卖点之后又跌了多少」= 保住的钱, 负数=卖飞)")
|
||||||
|
if not trails:
|
||||||
|
print(" 无样本。")
|
||||||
|
n_fired, tot_saved = 0, 0.0
|
||||||
|
for s in trails:
|
||||||
|
agg = fills_of(s["strategy_id"])
|
||||||
|
sl = agg["sell"]
|
||||||
|
code, sid = s.get("ts_code"), s["strategy_id"]
|
||||||
|
st = s.get("state") or {}
|
||||||
|
if sl["qty"] > 0:
|
||||||
|
n_fired += 1
|
||||||
|
sell_avg = sl["amt"] / sl["qty"]
|
||||||
|
cur = _f(px.get(code))
|
||||||
|
saved = (sell_avg - cur) * sl["qty"] if cur > 0 else None
|
||||||
|
if saved is not None:
|
||||||
|
tot_saved += saved
|
||||||
|
print(f" {code} {sid} [{s.get('status')}]: 触发过, "
|
||||||
|
f"卖 {sl['qty']} 股 (均 {sell_avg:.3f}), "
|
||||||
|
+ (f"现价 {cur:.3f} → 保住 {saved:+,.0f} 元" if saved is not None
|
||||||
|
else "无现价, 保住的钱先不算"))
|
||||||
|
else:
|
||||||
|
hw, armed = _f(st.get("high_water")), bool(st.get("armed"))
|
||||||
|
print(f" {code} {sid} [{s.get('status')}]: 未触发, "
|
||||||
|
f"高水位 {hw or '—'}, {'已武装' if armed else '未武装 (垫子还没到武装线)'}")
|
||||||
|
if trails and n_fired:
|
||||||
|
print(f" 合计: 触发 {n_fired}/{len(trails)} 条, 可算的保住 {tot_saved:+,.0f} 元"
|
||||||
|
+ ("" if n_fired >= MIN_SAMPLE else f" (样本不足 {MIN_SAMPLE} 条, 只报数不下结论)"))
|
||||||
|
|
||||||
|
# ---- 三、挂/未挂对照 ----
|
||||||
|
print(f"\n【三】挂/未挂对照 (近 {args.days} 天; 对照组=资格全同只是没轮上名额的票)")
|
||||||
|
try:
|
||||||
|
attached, control = ledger_window(args.days)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" ✗ 台账读取失败: {type(e).__name__}: {e}")
|
||||||
|
attached, control = [], []
|
||||||
|
px3 = current_prices([r.get("ts_code") for r in attached + control])
|
||||||
|
|
||||||
|
def _group(rows, label):
|
||||||
|
moves = []
|
||||||
|
for r in rows:
|
||||||
|
chg = _pct(r.get("price_at"), px3.get(r.get("ts_code")))
|
||||||
|
d = str(r.get("decided_at"))[:10]
|
||||||
|
print(f" {r.get('ts_code')} {d} 决定价 {_f(r.get('price_at')):.3f} → "
|
||||||
|
f"{_fmt_pct(chg)}")
|
||||||
|
if chg is not None:
|
||||||
|
moves.append(chg)
|
||||||
|
if moves:
|
||||||
|
avg = sum(moves) / len(moves)
|
||||||
|
print(f" {label}: {len(rows)} 次决定, 可算 {len(moves)} 次, 平均 {_fmt_pct(avg)}")
|
||||||
|
else:
|
||||||
|
print(f" {label}: {len(rows)} 次决定, 无一次可算 (决定价或现价缺失)")
|
||||||
|
return moves
|
||||||
|
|
||||||
|
if not attached and not control:
|
||||||
|
print(" 窗口内没有 ATTACH 留痕 —— 系统还没挂过, 或窗口太短。")
|
||||||
|
else:
|
||||||
|
print(" 挂上的:")
|
||||||
|
m1 = _group(attached, "挂上的")
|
||||||
|
print(" 名额挡下的 (对照):")
|
||||||
|
m2 = _group(control, "对照组")
|
||||||
|
if len(m1) >= MIN_SAMPLE and len(m2) >= MIN_SAMPLE:
|
||||||
|
print(f" 对照差: 挂上的平均 {_fmt_pct(sum(m1) / len(m1))} vs "
|
||||||
|
f"没轮上的 {_fmt_pct(sum(m2) / len(m2))} —— 差值为正说明排序在挑对票")
|
||||||
|
else:
|
||||||
|
print(f" 两组样本 ({len(m1)}/{len(m2)}) 不足 {MIN_SAMPLE}, 本期不下结论。")
|
||||||
|
|
||||||
|
print("\n判分口径提醒: 本报告只陈述事实读数, 阈值调整 (热度 0.80 / 日上限 2) "
|
||||||
|
"等两周以上样本再议; 精算逐笔账查 pms_lot 与台账。")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -27,10 +27,11 @@
|
||||||
在途与配额文案拆分; 减仓不掐策略腿 (12 例)
|
在途与配额文案拆分; 减仓不掐策略腿 (12 例)
|
||||||
test_batch16_units.py 软归档 archived_at: 单表守卫/只归终态/列表默认排除 (5 例)
|
test_batch16_units.py 软归档 archived_at: 单表守卫/只归终态/列表默认排除 (5 例)
|
||||||
test_batch17_units.py 策略自动挂载: 定性归类(子串+保守优先)/网格参数生成/
|
test_batch17_units.py 策略自动挂载: 定性归类(子串+保守优先)/网格参数生成/
|
||||||
连边矩阵/note 约定与冷却推导/接力判定/clear_buypause/
|
科创板一手200/连边矩阵/note 约定与冷却推导/接力判定/
|
||||||
编排冒烟(dry_run 滴水不写/名额/边三/边四全链) (34 例)
|
clear_buypause/编排冒烟(dry_run 滴水不写/名额/边三/
|
||||||
|
边四全链)/判分脚本聚合与对照分组 (36 例)
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
||||||
共 553 例
|
共 555 例
|
||||||
任一子集失败即整体失败 (退出码 1)。
|
任一子集失败即整体失败 (退出码 1)。
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,23 @@ def _():
|
||||||
assert gp and gp["step_pct"] == 0.005, gp
|
assert gp and gp["step_pct"] == 0.005, gp
|
||||||
|
|
||||||
|
|
||||||
|
@case("[网格] 科创板一手=200: 买得起判断与 per_lot 下限都按 200 (dry-run 实盘发现)")
|
||||||
|
def _():
|
||||||
|
assert adv.lot_of("688802.SH") == 200 and adv.lot_of("689009.SH") == 200
|
||||||
|
assert adv.lot_of("600000.SH") == 100 and adv.lot_of("300750.SZ") == 100
|
||||||
|
# 预算够 100 股不够 200 股: 主板挂得出, 科创板必须放弃并把 200 写进理由
|
||||||
|
gp, why = adv.grid_params(price=10.0, support=9.0, pressure=11.0, band=0.08,
|
||||||
|
step_pct=0.02, cap_room=3000.0, cap_ratio=0.5, lot=200)
|
||||||
|
assert gp is None and "一手(200股)" in why, (gp, why)
|
||||||
|
gp, _ = adv.grid_params(price=10.0, support=9.0, pressure=11.0, band=0.08,
|
||||||
|
step_pct=0.02, cap_room=3000.0, cap_ratio=0.5, lot=100)
|
||||||
|
assert gp and gp["per_lot"] == 100, gp
|
||||||
|
# 科创板预算充足但每档折出来不足 200 → 抬到 200 (仍是 100 的整数倍, runner 不会磨掉)
|
||||||
|
gp, _ = adv.grid_params(price=10.0, support=9.0, pressure=11.0, band=0.08,
|
||||||
|
step_pct=0.02, cap_room=16000.0, cap_ratio=0.5, lot=200)
|
||||||
|
assert gp and gp["per_lot"] == 200 and gp["per_lot"] % 100 == 0, gp
|
||||||
|
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# [三] 连边矩阵 (plan_edge)
|
# [三] 连边矩阵 (plan_edge)
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
@ -662,6 +679,39 @@ def _():
|
||||||
param_store.get_bool, adv._params = orig, orig_p
|
param_store.get_bool, adv._params = orig, orig_p
|
||||||
|
|
||||||
|
|
||||||
|
@case("[判分] report_strategy_score: 成交聚合按边分侧 / 对照只收名额挡下的 / 涨跌口径空值安全")
|
||||||
|
def _():
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import report_strategy_score as rep
|
||||||
|
orig = rep.fetch_all
|
||||||
|
try:
|
||||||
|
rep.fetch_all = lambda sql, p=None: [
|
||||||
|
{"side": "buy", "exec_qty": 300, "exec_avg_price": 10.0},
|
||||||
|
{"side": "buy", "exec_qty": 100, "exec_avg_price": 11.0},
|
||||||
|
{"side": "sell", "exec_qty": 200, "exec_avg_price": 10.8},
|
||||||
|
{"side": "weird", "exec_qty": 999, "exec_avg_price": 1.0}, # 未知边丢弃
|
||||||
|
]
|
||||||
|
agg = rep.fills_of("S_X")
|
||||||
|
assert agg["buy"] == {"qty": 400, "amt": 4100.0, "n": 2}, agg
|
||||||
|
assert agg["sell"]["qty"] == 200 and abs(agg["sell"]["amt"] - 2160.0) < 1e-6, agg
|
||||||
|
|
||||||
|
rep.fetch_all = lambda sql, p=None: [
|
||||||
|
{"ts_code": "A", "verdict": "PASS", "price_at": 10, "reason": "挂了"},
|
||||||
|
{"ts_code": "B", "verdict": "NOTE", "price_at": 9,
|
||||||
|
"reason": "想挂 accum_grid 但今日新挂名额已满, 留到明天"},
|
||||||
|
{"ts_code": "C", "verdict": "NOTE", "price_at": 8,
|
||||||
|
"reason": "想挂网格但放弃: 买不起一手"}, # 排除项, 不是对照
|
||||||
|
]
|
||||||
|
att, ctl = rep.ledger_window(30)
|
||||||
|
assert [r["ts_code"] for r in att] == ["A"], att
|
||||||
|
assert [r["ts_code"] for r in ctl] == ["B"], ctl
|
||||||
|
finally:
|
||||||
|
rep.fetch_all = orig
|
||||||
|
assert rep._pct(10.0, 11.0) is not None and abs(rep._pct(10.0, 11.0) - 0.1) < 1e-9
|
||||||
|
assert rep._pct(None, 11.0) is None and rep._pct(10.0, None) is None
|
||||||
|
assert rep._fmt_pct(None) == "—" and rep._fmt_pct(0.1) == "+10.00%"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
passed, failed = 0, []
|
passed, failed = 0, []
|
||||||
for name, fn in RESULTS:
|
for name, fn in RESULTS:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue