处理对应bug
This commit is contained in:
parent
05a4fee86c
commit
a555564aa1
17
README.md
17
README.md
|
|
@ -66,7 +66,7 @@ scripts/
|
||||||
test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 65 例
|
test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 65 例
|
||||||
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 32 例
|
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 32 例
|
||||||
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 60 例
|
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 60 例
|
||||||
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸双判据 42 例
|
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 47 例
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 58 例
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 58 例
|
||||||
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检)
|
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检)
|
||||||
check_db.py 实机连通性与表结构自检 (需真实 .env)
|
check_db.py 实机连通性与表结构自检 (需真实 .env)
|
||||||
|
|
@ -257,6 +257,19 @@ make t-gate # 随时: 规则闸/研判闸拒了什么、为什么
|
||||||
|
|
||||||
同一行业的只数上限(`PMS_SECTOR_MAX_NAMES`,默认 4)不受影响,照常生效。
|
同一行业的只数上限(`PMS_SECTOR_MAX_NAMES`,默认 4)不受影响,照常生效。
|
||||||
|
|
||||||
|
### 一手取整的记账:planned_amount 必须是取整后的实际值
|
||||||
|
|
||||||
|
三批各自向下取整到一手,所以实际买到的必然少于目标——**票越贵差得越多**。`plan_increase_exposure` 的「候选池新票」那支原本按**理论目标**累计,而同一个函数里「补既有持仓」那支按取整后的实际金额累计,隔二十行两种写法。
|
||||||
|
|
||||||
|
2026-07-31 实机撞上:命令进度写着 `planned_amount=600,000` / `gap=0`(读起来是「完全满足」),而 15 条方案的金额合计只有 573,883——**少买 26,117 元、账面上完全看不出来**,其中 136.73 元一股的 `002353.SZ` 一只就差了 10,528。除了报数不实,还有两个连带后果:`ctx` 里的组合市值虚高,后面几只票的上限校验按一个不存在的仓位算;循环也会提前收敛。
|
||||||
|
|
||||||
|
现在一律按实际金额记账,并把缺口拆成两种——混在一起会得出错误结论:
|
||||||
|
|
||||||
|
- `shortfall`:**没找到足够的票**(候选不够、被闸拒了)。这才是「命令没满足」,`ok` 只看它。
|
||||||
|
- `rounding_gap`:**一手取整的零头**。A 股买不了零股,这不是失败。
|
||||||
|
|
||||||
|
否则任何一次升仓都会因为几千元的取整零头被判成失败,真正的「候选不够」反而淹没在里面。`plan_open_target`(个股建仓)原来把 `gap` 写死 0,同一个毛病的另一面,一并修了。
|
||||||
|
|
||||||
### 零方案的任务命令置 CANCELLED,不是 DONE
|
### 零方案的任务命令置 CANCELLED,不是 DONE
|
||||||
|
|
||||||
一条**该**产出方案的任务命令一条都没产出,那不是「完成」,是「没发生」。原来它和开关类命令(`HALT_BUY` 这种下达即完成的)共用 `DONE`,后果是页面显示已完成、`cancel()` 又因为 `DONE` 不在 `ACTIVE_TASK_STATES` 里而拒绝撤销——用户既看不出没执行成,也退不回来,日报还会把它算进完成的命令。现在这种情况置 `CANCELLED` 并把原因写进 `note`。
|
一条**该**产出方案的任务命令一条都没产出,那不是「完成」,是「没发生」。原来它和开关类命令(`HALT_BUY` 这种下达即完成的)共用 `DONE`,后果是页面显示已完成、`cancel()` 又因为 `DONE` 不在 `ACTIVE_TASK_STATES` 里而拒绝撤销——用户既看不出没执行成,也退不回来,日报还会把它算进完成的命令。现在这种情况置 `CANCELLED` 并把原因写进 `note`。
|
||||||
|
|
@ -285,7 +298,7 @@ make t-gate # 随时: 规则闸/研判闸拒了什么、为什么
|
||||||
|
|
||||||
## 已实现 / 待开发
|
## 已实现 / 待开发
|
||||||
|
|
||||||
**已实现**:建表 DDL 与建表脚本;配置与运行参数中心;仓位规划器与安全垫账;命令系统(27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B(分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**(`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 346 例**。
|
**已实现**:建表 DDL 与建表脚本;配置与运行参数中心;仓位规划器与安全垫账;命令系统(27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B(分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**(`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 351 例**。
|
||||||
|
|
||||||
### 下一步(按可动工顺序)
|
### 下一步(按可动工顺序)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,8 @@ def plan_increase_exposure(*, add_amount: float, positions: list, candidates=Non
|
||||||
ctx = dict(ctx or {})
|
ctx = dict(ctx or {})
|
||||||
target = float(add_amount or 0)
|
target = float(add_amount or 0)
|
||||||
items, notes, rejects = [], [], []
|
items, notes, rejects = [], [], []
|
||||||
acc = 0.0
|
acc = 0.0 # 取整后**实际**能买到的金额
|
||||||
|
intended = 0.0 # 分配出去的**目标**金额 (acc 与它的差 = 一手取整的零头)
|
||||||
scale = float(ctx.get("scale") or 0)
|
scale = float(ctx.get("scale") or 0)
|
||||||
default_target = float(params.get("stock_target_default", 0.06))
|
default_target = float(params.get("stock_target_default", 0.06))
|
||||||
batch_split = params.get("batch_split") or (0.5, 0.25, 0.25)
|
batch_split = params.get("batch_split") or (0.5, 0.25, 0.25)
|
||||||
|
|
@ -266,6 +267,7 @@ def plan_increase_exposure(*, add_amount: float, positions: list, candidates=Non
|
||||||
items.append(_item(p["ts_code"], A_ADD, SIDE_BUY, q, amt, P_OPEN_BASE,
|
items.append(_item(p["ts_code"], A_ADD, SIDE_BUY, q, amt, P_OPEN_BASE,
|
||||||
f"升仓: 垫厚({float(p.get('cushion_pct') or 0):.1%})补至目标 "
|
f"升仓: 垫厚({float(p.get('cushion_pct') or 0):.1%})补至目标 "
|
||||||
f"{tgt_pct:.1%}", tier="1_补既有"))
|
f"{tgt_pct:.1%}", tier="1_补既有"))
|
||||||
|
intended += want_amt
|
||||||
acc += amt
|
acc += amt
|
||||||
ctx = _ctx_after(ctx, amt, is_new_name=False, sector=p.get("sector"))
|
ctx = _ctx_after(ctx, amt, is_new_name=False, sector=p.get("sector"))
|
||||||
|
|
||||||
|
|
@ -282,22 +284,42 @@ def plan_increase_exposure(*, add_amount: float, positions: list, candidates=Non
|
||||||
rejects.append({"ts_code": c.get("ts_code"), "reasons": [sp["reason"]]})
|
rejects.append({"ts_code": c.get("ts_code"), "reasons": [sp["reason"]]})
|
||||||
continue
|
continue
|
||||||
base = sp["batches"][0]
|
base = sp["batches"][0]
|
||||||
bad = check_all_caps(ts_code=c.get("ts_code"), add_amount=want_amt,
|
# **一律按取整后的实际金额记账, 不是按理论目标。** 三批各自向下取整到一手, 实际买到的
|
||||||
|
# 比 want_amt 少 —— 票越贵差得越多 (2026-07-31 实机: 136.73 元的 002353 一只就差了
|
||||||
|
# 10,528 元)。拿 want_amt 累计有三个后果: ① progress.planned_amount 报高、gap 报低,
|
||||||
|
# 页面说"完全满足、缺口 0"而实际少买了 4.4% ② ctx 里的组合市值虚高, 后面几只票的
|
||||||
|
# 上限校验按一个不存在的仓位算, 会提前把人拦掉 ③ 循环提前收敛。
|
||||||
|
# 上面①「补既有持仓」那支本来就是这么写的 (amt = q × price), 这里是漏了。
|
||||||
|
actual = sum(b["qty"] * price for b in sp["batches"])
|
||||||
|
bad = check_all_caps(ts_code=c.get("ts_code"), add_amount=actual,
|
||||||
ctx=_new_name_ctx(ctx, c))
|
ctx=_new_name_ctx(ctx, c))
|
||||||
if bad:
|
if bad:
|
||||||
rejects.append({"ts_code": c.get("ts_code"), "reasons": bad})
|
rejects.append({"ts_code": c.get("ts_code"), "reasons": bad})
|
||||||
continue
|
continue
|
||||||
items.extend(_open_batch_items(c.get("ts_code"), sp, price,
|
items.extend(_open_batch_items(c.get("ts_code"), sp, price,
|
||||||
reason_prefix="升仓建新仓"))
|
reason_prefix="升仓建新仓"))
|
||||||
acc += want_amt
|
intended += want_amt
|
||||||
ctx = _ctx_after(ctx, want_amt, is_new_name=True, sector=c.get("sector"))
|
acc += actual
|
||||||
notes.append(f"{c.get('ts_code')}: 目标 {want_amt:,.0f} 元, 首批 {base['qty']} 股")
|
ctx = _ctx_after(ctx, actual, is_new_name=True, sector=c.get("sector"))
|
||||||
|
notes.append(f"{c.get('ts_code')}: 目标 {want_amt:,.0f} 元 → 取整后 {actual:,.0f} 元, "
|
||||||
|
f"首批 {base['qty']} 股")
|
||||||
|
|
||||||
|
# 缺口分两种, 混在一起会得出错误结论:
|
||||||
|
# shortfall 没找到足够的票 (候选不够 / 被闸拒了) —— 这才是"命令没满足"
|
||||||
|
# rounding 一手取整的零头 —— 票越贵越大, 但它不是失败, 是 A 股买不了零股
|
||||||
|
# `ok` 只看 shortfall: 否则任何一次升仓都会因为几千元的取整零头被判成失败,
|
||||||
|
# 而真正的"候选不够"就淹没在里面了。
|
||||||
gap = max(0.0, target - acc)
|
gap = max(0.0, target - acc)
|
||||||
if gap > 0:
|
shortfall = max(0.0, target - intended)
|
||||||
notes.append(f"候选与补仓空间不足, 缺口 {gap:,.0f} 元")
|
rounding = max(0.0, intended - acc)
|
||||||
return {"ok": gap <= 0, "target_amount": round(target, 2), "planned_amount": round(acc, 2),
|
if shortfall > 0:
|
||||||
"gap": round(gap, 2), "items": items, "notes": notes, "rejects": rejects}
|
notes.append(f"候选与补仓空间不足, 缺口 {shortfall:,.0f} 元")
|
||||||
|
if rounding > 0:
|
||||||
|
notes.append(f"一手取整少买 {rounding:,.0f} 元 (不足一手的部分买不进去; 票价越高越明显)")
|
||||||
|
return {"ok": shortfall <= 0, "target_amount": round(target, 2),
|
||||||
|
"planned_amount": round(acc, 2), "gap": round(gap, 2),
|
||||||
|
"shortfall": round(shortfall, 2), "rounding_gap": round(rounding, 2),
|
||||||
|
"items": items, "notes": notes, "rejects": rejects}
|
||||||
|
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
|
|
@ -341,8 +363,15 @@ def plan_open_target(*, ts_code: str, target_pct: float, price: float, ctx: dict
|
||||||
notes = []
|
notes = []
|
||||||
if sp["scheme"] != tuple(batch_split):
|
if sp["scheme"] != tuple(batch_split):
|
||||||
notes.append(f"一手检查: 批次自动合并为 {sp['scheme']} (原 {tuple(batch_split)})")
|
notes.append(f"一手检查: 批次自动合并为 {sp['scheme']} (原 {tuple(batch_split)})")
|
||||||
|
# gap 原来写死 0 —— 但三批各自向下取整到一手, 实际买到的必然少于目标, 票越贵差越多。
|
||||||
|
# 报 0 等于说"完全建到目标了", 而页面上的占规模会比命令要求的低一截, 对不上。
|
||||||
|
gap = max(0.0, add_amount - planned)
|
||||||
|
if gap > 0:
|
||||||
|
notes.append(f"一手取整: 目标 {add_amount:,.0f} 元 → 实际 {planned:,.0f} 元, "
|
||||||
|
f"差 {gap:,.0f} 元 (不足一手的部分买不进去)")
|
||||||
return {"ok": True, "items": items, "target_amount": round(add_amount, 2),
|
return {"ok": True, "items": items, "target_amount": round(add_amount, 2),
|
||||||
"planned_amount": round(planned, 2), "gap": 0.0, "notes": notes, "rejects": []}
|
"planned_amount": round(planned, 2), "gap": round(gap, 2),
|
||||||
|
"notes": notes, "rejects": []}
|
||||||
|
|
||||||
|
|
||||||
def plan_exit_stock(*, ts_code: str, position: dict, reason: str = "清仓命令") -> dict:
|
def plan_exit_stock(*, ts_code: str, position: dict, reason: str = "清仓命令") -> dict:
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@
|
||||||
test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 (65 例)
|
test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 (65 例)
|
||||||
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 (32 例)
|
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 (32 例)
|
||||||
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 例)
|
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 例)
|
||||||
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸双判据 (42 例)
|
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 (47 例)
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
||||||
共 346 例
|
共 351 例
|
||||||
任一子集失败即整体失败 (退出码 1)。
|
任一子集失败即整体失败 (退出码 1)。
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,15 @@ def _():
|
||||||
assert len(adds) == 1 and adds[0]["ts_code"] == "600000.SH" and adds[0]["qty"] == 4000, adds
|
assert len(adds) == 1 and adds[0]["ts_code"] == "600000.SH" and adds[0]["qty"] == 4000, adds
|
||||||
new_codes = {i["ts_code"] for i in r["items"] if i["tier"].startswith("批次_")}
|
new_codes = {i["ts_code"] for i in r["items"] if i["tier"].startswith("批次_")}
|
||||||
assert new_codes == {"688981.SH", "000651.SZ"}, new_codes
|
assert new_codes == {"688981.SH", "000651.SZ"}, new_codes
|
||||||
assert r["ok"] and abs(r["planned_amount"] - 200_000) < 1e-6, r
|
# 两种缺口要分开 (2026-07-31 修): 目标 20 万全分配出去了 → shortfall=0 → ok;
|
||||||
|
# 但 000651.SZ 目标 4 万、15 元一股, 三批各自向下取整到一手只买到 37,500 —— 那 2,500
|
||||||
|
# 是一手取整的零头, 不是"没找到票"。**planned_amount 必须是取整后的实际值**, 否则
|
||||||
|
# 页面会说"完全满足、缺口 0"而实际少买 (07-31 实机就是这么报的)。
|
||||||
|
assert r["ok"] and r["shortfall"] == 0, r
|
||||||
|
assert abs(r["planned_amount"] - 197_500) < 1e-6, r["planned_amount"]
|
||||||
|
assert abs(r["rounding_gap"] - 2_500) < 1e-6, r["rounding_gap"]
|
||||||
|
assert abs(sum(i["amount"] for i in r["items"]) - r["planned_amount"]) < 1e-6, (
|
||||||
|
"planned_amount 必须等于各条方案金额之和")
|
||||||
|
|
||||||
|
|
||||||
@case("升仓命令·冻结票与非厚垫票不补仓")
|
@case("升仓命令·冻结票与非厚垫票不补仓")
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,86 @@ def t_g4():
|
||||||
assert "1 只" in s and "股A" in s
|
assert "1 只" in s and "股A" in s
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ [H] 一手取整的记账口径
|
||||||
|
# 2026-07-31 实机: 命令进度写着 planned_amount=600,000 / gap=0 ("完全满足"), 而 15 条方案
|
||||||
|
# 的金额合计只有 573,883 —— 少买 26,117 元 (4.4%) 且账面上完全看不出来。
|
||||||
|
# 原因是 plan_increase_exposure 的②分支按**理论目标**累计, 而①分支按取整后的实际金额累计,
|
||||||
|
# 同一个函数隔二十行两种写法。
|
||||||
|
from app.core import planner as pl # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _cx(**kw):
|
||||||
|
d = dict(scale=SCALE, portfolio_cap=0.60, stock_cap=0.08, max_names=15,
|
||||||
|
portfolio_mv=0.0, names_count=0, stock_mv=0.0, is_new_name=True,
|
||||||
|
sector=None, sector_names=0, sector_mv=0.0,
|
||||||
|
sector_max_names=4, sector_max_ratio=0.40)
|
||||||
|
d.update(kw)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@case("[H1] planned_amount 必须等于各条方案金额之和")
|
||||||
|
def t_h1():
|
||||||
|
r = pl.plan_increase_exposure(
|
||||||
|
add_amount=0.30 * SCALE, positions=[],
|
||||||
|
candidates=[{"ts_code": f"股{i}.SH", "price": p, "score": 1.0 - i * 0.01}
|
||||||
|
for i, p in enumerate([24.83, 25.22, 35.21, 136.73, 35.04])],
|
||||||
|
ctx=_cx(), params={"stock_target_default": 0.06})
|
||||||
|
assert abs(sum(i["amount"] for i in r["items"]) - r["planned_amount"]) < 1e-6, (
|
||||||
|
f"账面 {r['planned_amount']:,.0f} vs 方案合计 "
|
||||||
|
f"{sum(i['amount'] for i in r['items']):,.0f}")
|
||||||
|
|
||||||
|
|
||||||
|
@case("[H2] 高价股的取整损失要记进 rounding_gap, 不能被吞掉")
|
||||||
|
def t_h2():
|
||||||
|
# 136.73 元一股, 一手就要 13,673 元 —— 目标 12 万只能买到 800 股 = 109,384
|
||||||
|
r = pl.plan_increase_exposure(
|
||||||
|
add_amount=0.06 * SCALE, positions=[],
|
||||||
|
candidates=[{"ts_code": "002353.SZ", "price": 136.73, "score": 1.0}],
|
||||||
|
ctx=_cx(), params={"stock_target_default": 0.06})
|
||||||
|
assert abs(r["planned_amount"] - 109_384) < 1.0, r["planned_amount"]
|
||||||
|
assert abs(r["rounding_gap"] - (120_000 - 109_384)) < 1.0, r["rounding_gap"]
|
||||||
|
assert any("一手取整" in n for n in r["notes"]), r["notes"]
|
||||||
|
|
||||||
|
|
||||||
|
@case("[H3] 取整零头不算命令失败, 候选不够才算")
|
||||||
|
def t_h3():
|
||||||
|
# 候选够: 目标全分配出去了, 只是取整少买一点 → ok
|
||||||
|
ok_r = pl.plan_increase_exposure(
|
||||||
|
add_amount=0.06 * SCALE, positions=[],
|
||||||
|
candidates=[{"ts_code": "002353.SZ", "price": 136.73, "score": 1.0}],
|
||||||
|
ctx=_cx(), params={"stock_target_default": 0.06})
|
||||||
|
assert ok_r["ok"] is True and ok_r["shortfall"] == 0 and ok_r["rounding_gap"] > 0
|
||||||
|
# 候选不够: 要 30% 却只有一只票能给 6% → 不 ok
|
||||||
|
bad_r = pl.plan_increase_exposure(
|
||||||
|
add_amount=0.30 * SCALE, positions=[],
|
||||||
|
candidates=[{"ts_code": "002353.SZ", "price": 136.73, "score": 1.0}],
|
||||||
|
ctx=_cx(), params={"stock_target_default": 0.06})
|
||||||
|
assert bad_r["ok"] is False and bad_r["shortfall"] > 0
|
||||||
|
assert any("候选与补仓空间不足" in n for n in bad_r["notes"])
|
||||||
|
|
||||||
|
|
||||||
|
@case("[H4] 组合市值按实际金额推进, 不按理论目标 —— 否则后面几只被虚高的仓位拦掉")
|
||||||
|
def t_h4():
|
||||||
|
r = pl.plan_increase_exposure(
|
||||||
|
add_amount=0.60 * SCALE, positions=[],
|
||||||
|
candidates=[{"ts_code": f"股{i}.SH", "price": 136.73, "score": 1.0 - i * 0.01}
|
||||||
|
for i in range(10)],
|
||||||
|
ctx=_cx(), params={"stock_target_default": 0.06})
|
||||||
|
names = {i["ts_code"] for i in r["items"]}
|
||||||
|
assert len(names) == 10, (
|
||||||
|
f"十只高价股每只 6%, 按实际金额推进才装得下 10 只; 按理论目标会提前收敛, "
|
||||||
|
f"实际装了 {len(names)} 只")
|
||||||
|
|
||||||
|
|
||||||
|
@case("[H5] 个股建仓命令的 gap 也要如实反映取整损失")
|
||||||
|
def t_h5():
|
||||||
|
r = pl.plan_open_target(ts_code="002353.SZ", price=136.73, target_pct=0.06,
|
||||||
|
ctx=_cx(), params={})
|
||||||
|
assert r["ok"] and r["gap"] > 0, f"原来写死 gap=0, 等于说'完全建到目标了': {r}"
|
||||||
|
assert abs(r["planned_amount"] - sum(i["amount"] for i in r["items"])) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
import logging
|
import logging
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue