Files
sanguo_vnpy_v2/sanguo_portfolio/ishortfall.py
T
claude_dev e5b523de3f
CI/CD / test (push) Successful in 24s
CI/CD / nas-deploy (push) Successful in 9s
CI/CD / nas-verify (push) Successful in 6s
fix(pipeline): 终审修复波——价格源缓存键(日,码)修+分钟键形态解析+probe 形态门+norej 跨文件防撞+prev_close 20 日窗+费用零价跳过 [vps]
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 08:14:49 +08:00

88 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Perold 落地差距四分量纯函数计算器(spec §4.7 第1/2条)。
定性:paper 腿=无摩擦基准(Perold 原教旨=决策时刻以决策价建仓),四分量全部
定义在 live 腿;paper 每日一次 step 撮合下延迟/冲击≡机制性零——显式标注非缺数。
分量(正=逆风,方向 buy=+1/sell=−1):
延迟=(到达−决策)×方向×已成交量
冲击=(成交均−到达)×方向×成交量
机会=(收盘−决策)×方向×未成交量(Perold EOD 口径)
费用=Σfee(estimate_fee 口径,Task 1)
可加和恒等式:延迟+冲击+机会+费用 == Σ方向×[(成交均−决策)×qf+(收盘−决策)×qu]+费用
(identity_resid 透出,>1e-6 即回归)。qf>0 而 fills 空(超时行无逐笔价)时
恒等式出现缺口=identity_resid 非零——「缺成交明细」的诚实信号,非回归;
decision/arrival 有而 close 缺(或反之)的腿只进分量侧不进直算侧,resid
同样非零=缺数信号非回归。
价格移动参考行(Kissell-Glantz price appreciation,不入 is_total):
(收盘−决策)×方向×委托总量——大盘漂移语境,防漂移污染执行评价。
缺锚(decision/arrival/close 任一 None):该分量跳过、腿计入 legs_no_anchor
(显式可见,宁缺毋假),费用照算。
"""
from __future__ import annotations
from dataclasses import dataclass
PAPER_MECHANISM_ZERO_NOTE = (
"paper 腿延迟/冲击=机制性零(每日一次 step 撮合,定价=20:30 收盘回放);"
"显式标注非缺数,四分量全部定义在 live 腿")
@dataclass(frozen=True)
class OrderLeg:
action: str
qty: float
fills: tuple[tuple[float, float], ...] = ()
filled_qty: float | None = None # 重放权威已成交量(超时行无逐笔价场景)
decision_price: float | None = None
arrival_price: float | None = None
close_price: float | None = None
reject_reason: str | None = None
fee: float = 0.0
def compute_components(legs: list[OrderLeg]) -> dict:
delay = impact = opp = fee_total = movement = direct = 0.0
anchored = 0
traded_value = 0.0
by_reason = {k: 0.0 for k in
("T1", "CASH", "LIMIT_UP", "LIMIT_DOWN", "TIMEOUT")}
by_legs = {k: 0 for k in by_reason}
for leg in legs:
s = 1.0 if leg.action == "buy" else -1.0
qf = leg.filled_qty if leg.filled_qty is not None \
else sum(v for _, v in leg.fills)
qu = max(leg.qty - qf, 0.0)
fv = sum(v for _, v in leg.fills)
fill_avg = (sum(p * v for p, v in leg.fills) / fv) if fv else None
fee_total += leg.fee
traded_value += sum(p * v for p, v in leg.fills)
if leg.reject_reason:
by_legs[leg.reject_reason] = by_legs.get(leg.reject_reason, 0) + 1
if leg.decision_price is None:
continue
anchored += 1
dec = leg.decision_price
if qf and leg.arrival_price is not None:
delay += s * (leg.arrival_price - dec) * qf
if fill_avg is not None and leg.arrival_price is not None:
impact += s * (fill_avg - leg.arrival_price) * qf
if qu and leg.close_price is not None:
opp += s * (leg.close_price - dec) * qu
if leg.reject_reason:
by_reason[leg.reject_reason] = by_reason.get(
leg.reject_reason, 0.0) + s * (leg.close_price - dec) * qu
if leg.close_price is not None:
movement += s * (leg.close_price - dec) * leg.qty
if leg.close_price is not None and (fill_avg is not None or qf == 0):
fill_term = (fill_avg - dec) * qf if fill_avg is not None else 0.0
direct += s * (fill_term + (leg.close_price - dec) * qu) + leg.fee
total = delay + impact + opp + fee_total
return {"delay_cost": delay, "impact_cost": impact,
"opportunity_cost": opp, "fee_total": fee_total,
"price_movement": movement, "is_total": total,
"identity_resid": abs(total - direct),
"legs_count": len(legs), "legs_no_anchor": len(legs) - anchored,
"traded_value": traded_value, "by_reason": by_reason,
"by_reason_legs": by_legs,
"paper": {"mechanism_zero": True, "note": PAPER_MECHANISM_ZERO_NOTE}}