添加交易逻辑

This commit is contained in:
zlt 2026-08-25 13:57:16 +08:00
parent f4123d36d3
commit e4c23ec238
5 changed files with 329 additions and 11 deletions

View File

@ -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 PASS553 例)。判收建议:部署当天先 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 PASSdry-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 且它连网格都挂不上),排查后单独修
--- ---

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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: