tradingSystem/scripts/test_batch36_units.py

341 lines
17 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""任务命令结算按方案额 (2026-10-08)。后端离线单测, 不连库。
由来: 建仓 / 升仓 / 个股减仓这类任务命令, 方案全部成交也到不了 DONE。命令目标
(progress.target_amount) 是「规模 × 比例 − 现有市值」的理论金额, 没取整到手; 方案是整手的,
方案额几乎总比目标少一截零头。结算拿理论目标当完成线, 一股不差地成交完也够不着 ——
命令一直挂在执行中、窗口末转部分完成, 同股重下被判重复、暂停买入与一键清仓报冲突。
开发机实算: 现价 10.37 建仓 6% × 100 万, 目标 60,000、方案 59,109, 全部成交仍差 891 元。
修法: 结算线 = min(目标, 方案额) − 未解锁 GATED 批金额 (command_spec.effective_target)。
这里测三块:
A effective_target 纯逻辑: 取整零头、方案额高于目标、GATED 扣减与边界。
B 方案生成接结算: plan_open_target / plan_reduce_stock 产出的方案全部成交即判 DONE。
C 服务层 refresh_progress (内存桩): 建仓端到端与同股重下、HALT 行不进方案额、
方案没凑够目标的口径、没成交完的照旧不放过。
"""
import unit_test_guard # noqa: F401,单独运行也禁止联网
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.core import command_spec as cs # noqa: E402
from app.core import planner as pl # noqa: E402
RESULTS = []
PX = 10.37 # 开发机实算用的现价
SCALE = 1_000_000.0
def case(name):
def deco(fn):
RESULTS.append((name, fn))
return fn
return deco
def _ctx(**kw):
"""plan_open_target 的组合上下文: 空仓、上限宽松, 只让取整这一件事起作用。"""
c = {"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}
c.update(kw)
return c
def _settle(target, items, filled_ratio=1.0, window_over=False):
"""把方案条目当作非 GATED 批按比例成交, 走一遍与 refresh_progress 相同的结算口径。"""
planned = sum(i["amount"] for i in items if i["action"] != pl.A_HALT)
gated = sum(i["amount"] for i in items if i.get("gated"))
done = sum(i["amount"] * filled_ratio for i in items
if not i.get("gated") and i["action"] != pl.A_HALT)
return cs.settle_task_status(cs.effective_target(target, planned, gated), done, window_over)
# ================================================================ A 纯逻辑
@case("A1 一手取整零头: 目标 60,000、方案 59,109 全部成交 → 判 DONE (旧口径永远够不着)")
def _():
eff = cs.effective_target(60_000.0, 59_109.0, 0.0)
assert abs(eff - 59_109.0) < 1e-9, eff
assert cs.settle_task_status(eff, 59_109.0, False) == cs.ST_DONE
# 钉住由来: 旧口径拿理论目标当线, 全部成交仍差 891 元, 窗口内执行中、窗口后部分完成
assert cs.settle_task_status(60_000.0, 59_109.0, False) == cs.ST_EXECUTING
assert cs.settle_task_status(60_000.0, 59_109.0, True) == cs.ST_PARTIAL
@case("A2 方案额高于目标 (清弱票整票卖、升仓取整微超): 结算线仍是目标, 没够目标不判 DONE")
def _():
eff = cs.effective_target(200_000.0, 201_500.0, 0.0)
assert eff == 200_000.0, eff
assert cs.settle_task_status(eff, 150_000.0, False) == cs.ST_EXECUTING
assert cs.settle_task_status(eff, 200_000.0, False) == cs.ST_DONE
@case("A3 GATED 扣减口径不变; 结算线不为负; 空值按 0")
def _():
# 三批建仓: 方案 58,072 里 FILL/ADD 两批 29,036 是 GATED → 结算线 = 首批 29,036
assert abs(cs.effective_target(60_000.0, 58_072.0, 29_036.0) - 29_036.0) < 1e-9
assert cs.effective_target(10_000.0, 5_000.0, 8_000.0) == 0.0
assert cs.effective_target(None, None) == 0.0
assert cs.effective_target(50_000.0, None) == 50_000.0
# ================================================================ B 方案生成接结算
@case("B1 建仓 10.37 元 × 6%: 单批与 50/25/25 两种拆法, 首批全部成交都判 DONE; 没成交完不放过")
def _():
one = pl.plan_open_target(ts_code="600000.SH", target_pct=0.06, price=PX, ctx=_ctx(),
params={"batch_split": (1.0,)})
assert one["target_amount"] == 60_000.0 and one["planned_amount"] == 59_109.0, one
assert len(one["items"]) == 1 and one["items"][0]["qty"] == 5_700, one["items"]
three = pl.plan_open_target(ts_code="600000.SH", target_pct=0.06, price=PX, ctx=_ctx(),
params={"batch_split": (0.5, 0.25, 0.25)})
assert [i["qty"] for i in three["items"]] == [2_800, 1_400, 1_400], three["items"]
assert [bool(i.get("gated")) for i in three["items"]] == [False, True, True]
for r in (one, three):
assert _settle(r["target_amount"], r["items"]) == cs.ST_DONE, r
assert _settle(r["target_amount"], r["items"], filled_ratio=0.9) == cs.ST_EXECUTING
assert _settle(r["target_amount"], r["items"], filled_ratio=0.9,
window_over=True) == cs.ST_PARTIAL
@case("B2 个股减至 5%: 持 10,000 股 @10.37, 超出 53,700 → 方案 5,100 股 52,887, 全部成交判 DONE")
def _():
r = pl.plan_reduce_stock(ts_code="600000.SH", target_pct=0.05,
position={"total_qty": 10_000, "price": PX}, scale=SCALE)
assert abs(r["target_amount"] - 53_700.0) < 1e-6, r
assert r["items"][0]["qty"] == 5_100 and abs(r["planned_amount"] - 52_887.0) < 1e-6, r
assert _settle(r["target_amount"], r["items"]) == cs.ST_DONE
assert _settle(r["target_amount"], r["items"], filled_ratio=0.98) == cs.ST_EXECUTING
# ================================================================ C 服务层
def _fill(fake, plans):
for p in plans:
fake.update_plan(p["plan_id"], status="DONE", filled_qty=p["qty"])
@case("C1 建仓端到端 (50/25/25): 首批成交完即 DONE、GATED 批照记; 结算前同股重下判重复, 结算后放行")
def _():
from test_wiring import install_fakes
from app.services import command_service as csvc
fake = install_fakes(prices={"600000.SH": PX}, params={"PMS_TOTAL_SCALE": str(int(SCALE))})
r = csvc.issue("OPEN_TARGET", {"ts_code": "600000.SH", "target_pct": "6%"})
assert r["ok"] and r["status"] == "EXECUTING", r
cid = r["command_id"]
plans = fake.list_plans(command_id=cid)
base = [p for p in plans if p["status"] == "PENDING"]
assert len(base) == 1 and base[0]["qty"] == 2_800, plans
csvc.refresh_progress(cid)
assert fake.commands[cid]["status"] == "EXECUTING", fake.commands[cid]
dup = csvc.issue("OPEN_TARGET", {"ts_code": "600000.SH", "target_pct": "6%"})
assert not dup["ok"] and any("重复下达" in c["reason"] for c in dup["conflicts"]), dup
_fill(fake, base)
csvc.refresh_progress(cid)
c = fake.commands[cid]
assert c["status"] == "DONE", c
# 页面照实显示: 目标仍是 60,000, 已成交 29,036, GATED 两批 29,036 单列
assert c["progress"]["target_amount"] == 60_000.0, c["progress"]
assert abs(c["progress"]["done_amount"] - 29_036.0) < 1e-6, c["progress"]
assert abs(c["progress"]["gated_amount"] - 29_036.0) < 1e-6, c["progress"]
assert [p["status"] for p in fake.list_plans(command_id=cid)] == ["DONE", "CANCELLED", "CANCELLED"]
again = csvc.issue("OPEN_TARGET", {"ts_code": "600000.SH", "target_pct": "6%"})
assert again["ok"] and not again.get("conflicts"), again
@case("C2 建仓单批 (开发机实算口径): 方案 59,109 全部成交 → DONE, 进度照记目标 60,000")
def _():
from test_wiring import install_fakes
from app.services import command_service as csvc
fake = install_fakes(prices={"600000.SH": PX},
params={"PMS_TOTAL_SCALE": str(int(SCALE)), "PMS_BATCH_SPLIT": "1.0"})
r = csvc.issue("OPEN_TARGET", {"ts_code": "600000.SH", "target_pct": "6%"})
assert r["ok"], r
cid = r["command_id"]
plans = fake.list_plans(command_id=cid)
assert len(plans) == 1 and plans[0]["qty"] == 5_700, plans
_fill(fake, plans)
out = csvc.refresh_progress(cid)
assert out["commands"][0]["status"] == "DONE", out
prog = fake.commands[cid]["progress"]
assert prog["target_amount"] == 60_000.0 and abs(prog["done_amount"] - 59_109.0) < 1e-6, prog
@case("C3 撤单 HALT 行不进方案额: 降仓顺手撤的在途买单金额再大, 卖出方案成交完即 DONE")
def _():
from test_wiring import install_fakes
from app.services import command_service as csvc
fake = install_fakes(params={"PMS_TOTAL_SCALE": str(int(SCALE))})
fake.insert_command(command_id="CMD_H1", cmd_class=cs.CLS_TASK, cmd_type="REDUCE_EXPOSURE",
ts_code=None, params={}, status="EXECUTING",
progress={"target_amount": 50_000.0, "planned_amount": 49_800.0,
"deadline": "2099-01-01"})
fake.insert_plans([
{"plan_id": "PL_H1", "command_id": "CMD_H1", "ts_code": "000001.SZ", "action": "HALT",
"qty": 8_000, "amount": 80_000.0, "status": "DONE", "filled_qty": 0,
"priority": 10, "deadline": "2099-01-01"},
{"plan_id": "PL_H2", "command_id": "CMD_H1", "ts_code": "600000.SH", "action": "TRIM",
"qty": 4_800, "amount": 49_800.0, "status": "DONE", "filled_qty": 4_800,
"priority": 30, "deadline": "2099-01-01"},
])
csvc.refresh_progress("CMD_H1")
# HALT 行若算进方案额: min(50,000, 129,800) = 50,000 > 49,800, 又卡住
assert fake.commands["CMD_H1"]["status"] == "DONE", fake.commands["CMD_H1"]
@case("C4 方案本身没凑够目标 (升仓候选不够): 方案全部成交判 DONE, 缺口原样留在进度里")
def _():
from test_wiring import install_fakes
from app.services import command_service as csvc
fake = install_fakes(params={"PMS_TOTAL_SCALE": str(int(SCALE))})
fake.insert_command(command_id="CMD_S1", cmd_class=cs.CLS_TASK,
cmd_type="INCREASE_EXPOSURE", ts_code=None, params={},
status="EXECUTING",
progress={"target_amount": 200_000.0, "planned_amount": 120_000.0,
"gap": 80_000.0, "deadline": "2099-01-01"})
fake.insert_plans([
{"plan_id": "PL_S1", "command_id": "CMD_S1", "ts_code": "600000.SH", "action": "OPEN",
"qty": 12_000, "amount": 120_000.0, "status": "DONE", "filled_qty": 12_000,
"priority": 10, "deadline": "2099-01-01"},
])
csvc.refresh_progress("CMD_S1")
c = fake.commands["CMD_S1"]
assert c["status"] == "DONE", c
assert c["progress"]["gap"] == 80_000.0 and c["progress"]["target_amount"] == 200_000.0, c
# 降仓可卖不够那一侧的方案注记同步改口: 不再承诺「置部分完成」
r = pl.plan_reduce_exposure(release_amount=10_000_000, positions=[
{"ts_code": "600000.SH", "price": 10.0, "total_qty": 1_000, "base_qty": 1_000,
"cushion_pct": 0.05, "neg_cushion_days": 0}])
gap_note = [n for n in r["notes"] if "可减持仓不足" in n]
assert gap_note and "部分完成" not in gap_note[0] and "缺口" in gap_note[0], r["notes"]
@case("C5 没成交完的照旧不放过: 成交一半窗口内 EXECUTING、窗口过后 PARTIAL; 有未出清方案不判 DONE")
def _():
from test_wiring import install_fakes
from app.services import command_service as csvc
fake = install_fakes(params={"PMS_TOTAL_SCALE": str(int(SCALE))})
for cid, dl in (("CMD_P1", "2099-01-01"), ("CMD_P2", "2000-01-01")):
fake.insert_command(command_id=cid, cmd_class=cs.CLS_TASK, cmd_type="OPEN_TARGET",
ts_code="600000.SH", params={}, status="EXECUTING",
progress={"target_amount": 60_000.0, "deadline": dl})
fake.insert_plans([
{"plan_id": f"PL_{cid}", "command_id": cid, "ts_code": "600000.SH",
"action": "OPEN", "qty": 5_700, "amount": 59_109.0, "status": "EXECUTING",
"filled_qty": 2_800, "priority": 10, "deadline": dl}])
csvc.refresh_progress("CMD_P1")
csvc.refresh_progress("CMD_P2")
assert fake.commands["CMD_P1"]["status"] == "EXECUTING", fake.commands["CMD_P1"]
assert fake.commands["CMD_P2"]["status"] == "PARTIAL", fake.commands["CMD_P2"]
# 金额都到了但还有一条 0 金额方案没出清 (取不到现价的整票卖单) → 不判 DONE
fake.insert_command(command_id="CMD_P3", cmd_class=cs.CLS_TASK, cmd_type="EXIT_STOCK",
ts_code="600000.SH", params={}, status="EXECUTING",
progress={"target_amount": 30_000.0, "deadline": "2099-01-01"})
fake.insert_plans([
{"plan_id": "PL_P3a", "command_id": "CMD_P3", "ts_code": "600000.SH", "action": "EXIT",
"qty": 3_000, "amount": 30_000.0, "status": "DONE", "filled_qty": 3_000,
"priority": 20, "deadline": "2099-01-01"},
{"plan_id": "PL_P3b", "command_id": "CMD_P3", "ts_code": "000001.SZ", "action": "EXIT",
"qty": 1_000, "amount": 0.0, "status": "PENDING", "filled_qty": 0,
"priority": 20, "deadline": "2099-01-01"}])
csvc.refresh_progress("CMD_P3")
assert fake.commands["CMD_P3"]["status"] == "EXECUTING", fake.commands["CMD_P3"]
@case("C6 非零目标但方案为空不判完成")
def _():
from test_wiring import install_fakes
from app.services import command_service as svc
fake = install_fakes()
fake.insert_command(command_id="EMPTY", cmd_class=cs.CLS_TASK, cmd_type="OPEN_TARGET",
ts_code="600000.SH", params={}, status="EXECUTING",
progress={"target_amount": 60_000, "deadline": "2099-01-01"})
svc.refresh_progress("EMPTY")
assert fake.commands["EMPTY"]["status"] == "EXECUTING"
@case("C7 完成清理失败不置 DONE,取消成功后进程失败仍可重试")
def _():
from test_wiring import install_fakes
from app.repo import pms_repo
from app.services import command_service as svc
for stage in ("save", "cancel", "final"):
fake = install_fakes(prices={"600000.SH": PX}, params={"PMS_TOTAL_SCALE": str(int(SCALE))})
cmd = svc.issue("OPEN_TARGET", {"ts_code": "600000.SH", "target_pct": "6%"})
cid = cmd["command_id"]
_fill(fake, fake.list_plans(command_id=cid, statuses=["PENDING"]))
update, cancel = pms_repo.update_command, pms_repo.cancel_gated_plans_of_command
def broken_update(cid, **kw):
if (stage == "save" and kw.get("status") is None) or (stage == "final" and kw.get("status") == "DONE"):
return 0
return update(cid, **kw)
try:
pms_repo.update_command = broken_update
if stage == "cancel":
pms_repo.cancel_gated_plans_of_command = lambda cid: 0
try:
svc.refresh_progress(cid)
raise AssertionError("结算写入失败必须报错")
except RuntimeError:
pass
assert fake.commands[cid]["status"] == "EXECUTING"
if stage in ("save", "cancel"):
assert len(fake.list_plans(command_id=cid, statuses=["GATED"])) == 2
finally:
pms_repo.update_command, pms_repo.cancel_gated_plans_of_command = update, cancel
svc.refresh_progress(cid)
assert fake.commands[cid]["status"] == "DONE"
assert not fake.list_plans(command_id=cid, statuses=["GATED"])
assert fake.commands[cid]["progress"]["settlement_gated_amount"] == 29_036.0
@case("C8 只取消本命令 GATED,不改在途与其他命令;真实 SQL 限定同一范围")
def _():
import importlib.util
from pathlib import Path
from test_wiring import install_fakes
fake = install_fakes()
for i, (cid, status) in enumerate([("A", "GATED"), ("A", "EXECUTING"), ("A", "PENDING"), ("B", "GATED")]):
fake.insert_plans([{"plan_id": str(i), "command_id": cid, "status": status}])
assert fake.cancel_gated_plans_of_command("A") == 1
assert [p["status"] for p in fake.plans] == ["CANCELLED", "EXECUTING", "PENDING", "GATED"]
path = Path(__file__).resolve().parents[1] / "app/repo/pms_repo.py"
spec = importlib.util.spec_from_file_location("repo_sql_check", path)
repo = importlib.util.module_from_spec(spec)
spec.loader.exec_module(repo)
calls = []
repo.execute = lambda sql, params: calls.append((sql, params)) or 1
assert repo.cancel_gated_plans_of_command("A") == 1
sql, params = calls[0]
from app.db.session import assert_single_table
assert_single_table(sql)
assert "WHERE command_id = :cid AND status = 'GATED'" in sql and params["cid"] == "A"
def main():
ok = 0
for name, fn in RESULTS:
try:
fn()
ok += 1
print(" ok " + name)
except Exception:
import traceback
print(" FAIL " + name)
traceback.print_exc()
print("-" * 60)
if ok == len(RESULTS):
print("ALL PASS (%d cases)" % ok)
return 0
print("FAILED %d/%d" % (len(RESULTS) - ok, len(RESULTS)))
return 1
if __name__ == "__main__":
sys.exit(main())