2026-07-28 09:10:07 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
2026-08-03 12:49:01 +08:00
|
|
|
|
择时执行器 · 实现B「内置保守择时」+ 实现A的纯逻辑部件 (零外部依赖, 可单测)
|
|
|
|
|
|
========================================================================
|
|
|
|
|
|
设计 POSITION_MGMT_DESIGN.md §8。两个实现的分工 (2026-08-03 接实现A时定下):
|
|
|
|
|
|
|
|
|
|
|
|
hard_gate() 事实性检查 —— 无价/停牌/配额尽/非时段/一字板/不追高(当日涨幅)/
|
|
|
|
|
|
14:45 兜底。这些检查不委托给任何人, 两个择时实现都必须先过它;
|
|
|
|
|
|
命中返回决策, 未命中返回 None。
|
|
|
|
|
|
decide() 实现B 全量判定 = hard_gate() + 内置保守规则 (避开开盘/均价/回踩)。
|
|
|
|
|
|
行为与拆分前完全一致, test_batch3 锁着。
|
|
|
|
|
|
apply_advice() 把决策系统的应答 (FIRE/WAIT + 建议价) 折算成与 decide() 同构的决策。
|
|
|
|
|
|
建议价通常是执行区间的边缘; 偏离现价超出保护幅度时按本地口径重定并留痕。
|
|
|
|
|
|
|
|
|
|
|
|
实现A的取数与降级在 services/exec_advisor.py: 决策系统不可用 → 整轮退 decide()
|
|
|
|
|
|
(设计 §13「择时退实现B」), 绝不因为对端故障停出手。
|
2026-07-28 09:10:07 +08:00
|
|
|
|
|
|
|
|
|
|
规则原文与落点:
|
|
|
|
|
|
每日配额 = 剩余量 ÷ 剩余窗口天数, 向上取整到一手 → daily_quota()
|
|
|
|
|
|
卖出: 避开开盘 30 分钟; 现价 ≥ 当日均价(VWAP) 时分笔卖 → decide() side=sell
|
|
|
|
|
|
14:45 未完成 → 现价 × 0.998 限价兜底
|
|
|
|
|
|
买入: 现价 ≤ 当日均价或进入回踩带时买; 当日涨幅 > 5% 停止买入 (不追高)
|
|
|
|
|
|
窗口末日 14:45 强制限价完成或按命令属性作废
|
|
|
|
|
|
停牌/一字板当日跳过顺延; 窗口耗尽未完成 → 命令置「部分完成」并告警
|
|
|
|
|
|
|
|
|
|
|
|
时点一律用「分钟数」比较 (hm_to_min), 免去跨时区与字符串比较的坑。
|
|
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from app.core.sizer import LOT
|
|
|
|
|
|
|
|
|
|
|
|
OPEN_MIN = 9 * 60 + 30 # 09:30 开盘
|
|
|
|
|
|
CLOSE_MIN = 15 * 60 # 15:00 收盘
|
|
|
|
|
|
LUNCH_START = 11 * 60 + 30
|
|
|
|
|
|
LUNCH_END = 13 * 60
|
|
|
|
|
|
|
|
|
|
|
|
# decide() 的动作词
|
|
|
|
|
|
ACT_FIRE = "FIRE" # 出手
|
|
|
|
|
|
ACT_WAIT = "WAIT" # 条件未到, 本轮不动
|
|
|
|
|
|
ACT_SKIP = "SKIP" # 当日跳过 (停牌/一字板), 顺延
|
|
|
|
|
|
ACT_STOP = "STOP" # 本日不再出手 (如买入触发不追高)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def hm_to_min(hm) -> int:
|
|
|
|
|
|
""""14:45" / datetime / (h, m) → 当日分钟数。"""
|
|
|
|
|
|
if hm is None:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
if isinstance(hm, (tuple, list)):
|
|
|
|
|
|
return int(hm[0]) * 60 + int(hm[1])
|
|
|
|
|
|
if hasattr(hm, "hour"):
|
|
|
|
|
|
return hm.hour * 60 + hm.minute
|
|
|
|
|
|
s = str(hm).strip()
|
|
|
|
|
|
if ":" in s:
|
|
|
|
|
|
h, m = s.split(":")[:2]
|
|
|
|
|
|
return int(h) * 60 + int(m)
|
|
|
|
|
|
return int(s)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def in_session(now_min: int) -> bool:
|
|
|
|
|
|
return (OPEN_MIN <= now_min <= LUNCH_START) or (LUNCH_END <= now_min <= CLOSE_MIN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def daily_quota(remaining_qty: int, tdays_left: int, *, total_qty: int = None,
|
|
|
|
|
|
lot: int = LOT, allow_odd_tail: bool = False) -> int:
|
|
|
|
|
|
"""当日配额 = 剩余量 ÷ 剩余交易日, 向上取整到一手。
|
|
|
|
|
|
|
|
|
|
|
|
* 最后一日 (tdays_left ≤ 1) 或剩余不足一手 → 全部剩余 (含零股尾巴)。
|
|
|
|
|
|
* allow_odd_tail=True (整票清仓) 时不做整百取整, 零股一并出。
|
|
|
|
|
|
"""
|
|
|
|
|
|
r = max(0, int(remaining_qty or 0))
|
|
|
|
|
|
if r <= 0:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
d = max(1, int(tdays_left or 1))
|
|
|
|
|
|
if d <= 1 or r <= lot:
|
|
|
|
|
|
return r
|
|
|
|
|
|
raw = r / d
|
|
|
|
|
|
q = int(-(-raw // lot)) * lot # 向上取整到一手
|
|
|
|
|
|
q = min(q, r)
|
|
|
|
|
|
if not allow_odd_tail and q % lot and q != r:
|
|
|
|
|
|
q = (q // lot) * lot
|
|
|
|
|
|
# 若本次取整后剩下不足一手的尾巴, 并进本次一起出, 免得最后一天剩 30 股卡住
|
|
|
|
|
|
if 0 < r - q < lot:
|
|
|
|
|
|
q = r
|
|
|
|
|
|
return max(q, 0) if q > 0 else r
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def slice_qty(quota: int, slices: int = 1, lot: int = LOT) -> list:
|
|
|
|
|
|
"""把当日配额切成 N 笔 (设计「分笔卖出配额」)。最后一笔兜底吃掉余数。"""
|
|
|
|
|
|
quota = max(0, int(quota or 0))
|
|
|
|
|
|
n = max(1, int(slices or 1))
|
|
|
|
|
|
if quota <= 0:
|
|
|
|
|
|
return []
|
|
|
|
|
|
if n == 1 or quota <= lot:
|
|
|
|
|
|
return [quota]
|
|
|
|
|
|
per = int(quota / n / lot) * lot
|
|
|
|
|
|
if per <= 0:
|
|
|
|
|
|
return [quota]
|
|
|
|
|
|
out = [per] * (n - 1)
|
|
|
|
|
|
out.append(quota - per * (n - 1))
|
|
|
|
|
|
return [q for q in out if q > 0]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 12:49:01 +08:00
|
|
|
|
def hard_gate(*, side: str, now, day: dict, params: dict, is_last_day: bool,
|
2026-08-06 15:25:39 +08:00
|
|
|
|
is_command: bool, fired_today: int = 0, quota: int = 0):
|
2026-08-03 12:49:01 +08:00
|
|
|
|
"""事实性检查 (实现A/B 共用的前置)。命中返回决策 dict, 未命中返回 None。
|
2026-07-28 09:10:07 +08:00
|
|
|
|
|
2026-08-03 12:49:01 +08:00
|
|
|
|
包含: 无价/停牌/配额尽/非时段/一字板/买入不追高(当日涨幅)/14:45 兜底与「兜底后
|
|
|
|
|
|
不新开买单」。**不含**避开开盘 N 分钟与均价/回踩 —— 那些是各实现自己的规则。
|
|
|
|
|
|
|
|
|
|
|
|
与拆分前 decide() 的唯一语义差别: 卖出的 14:45 兜底现在排在「避开开盘 30 分钟」
|
|
|
|
|
|
之前判。两者只在 eod_force_time 被改到 10:00 之前这种病态配置下才会同时成立,
|
|
|
|
|
|
且真到那时也该是兜底赢 —— 强制完成这件事永远归 PMS 自己管。
|
2026-08-06 15:25:39 +08:00
|
|
|
|
|
|
|
|
|
|
**is_command 是必填的, 故意不给默认值** (2026-08-06): 漏传立刻 TypeError,
|
|
|
|
|
|
而不是悄悄按某一侧的口径走。它决定窗口末日要不要强制完成 —— 见下面买入分支的说明。
|
2026-07-28 09:10:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
now_min = hm_to_min(now)
|
|
|
|
|
|
price = float(day.get("price") or 0)
|
|
|
|
|
|
eod_min = hm_to_min(params.get("eod_force_time") or "14:45")
|
|
|
|
|
|
disc = float(params.get("eod_force_discount") or 0.998)
|
|
|
|
|
|
left = max(0, int(quota) - int(fired_today))
|
|
|
|
|
|
|
|
|
|
|
|
def out(action, reason, limit=None, forced=False):
|
|
|
|
|
|
return {"action": action, "qty_hint": left, "limit_price": limit,
|
|
|
|
|
|
"reason": reason, "forced": forced}
|
|
|
|
|
|
|
|
|
|
|
|
if price <= 0:
|
|
|
|
|
|
return out(ACT_SKIP, "取不到现价, 当日跳过")
|
|
|
|
|
|
if day.get("halted"):
|
|
|
|
|
|
return out(ACT_SKIP, "停牌, 当日跳过顺延")
|
|
|
|
|
|
if left <= 0:
|
|
|
|
|
|
return out(ACT_WAIT, "当日配额已出完")
|
|
|
|
|
|
if not in_session(now_min):
|
|
|
|
|
|
return out(ACT_WAIT, "非交易时段")
|
|
|
|
|
|
|
|
|
|
|
|
if side == "sell":
|
2026-08-06 15:25:39 +08:00
|
|
|
|
# **卖出侧不按命令/自主分岔, 一律兜底。** 这不是漏改, 是方向不同:
|
|
|
|
|
|
# 买入的强制完成是"多背一份风险", 卖出的强制完成是"少背一份风险"。
|
|
|
|
|
|
# 自主减仓 (保垫减仓、信号转来的清仓) 若也到期作废, 那是把该降的风险留在账上 ——
|
|
|
|
|
|
# 宁可买不上, 但不能卖不掉。减持方向不设门槛这条口径, 在这里同样成立。
|
2026-07-28 09:10:07 +08:00
|
|
|
|
if day.get("limit_down") and not is_last_day:
|
|
|
|
|
|
return out(ACT_SKIP, "跌停一字板, 当日跳过顺延")
|
|
|
|
|
|
if now_min >= eod_min:
|
|
|
|
|
|
return out(ACT_FIRE, f"{_fmt(eod_min)} 兜底: 限价 = 现价×{disc}",
|
|
|
|
|
|
limit=round(price * disc, 2), forced=True)
|
2026-08-03 12:49:01 +08:00
|
|
|
|
return None
|
2026-07-28 09:10:07 +08:00
|
|
|
|
|
|
|
|
|
|
if side == "buy":
|
|
|
|
|
|
if day.get("limit_up"):
|
|
|
|
|
|
return out(ACT_SKIP, "涨停一字板, 当日跳过顺延")
|
|
|
|
|
|
dayup = day.get("day_chg_from_open")
|
|
|
|
|
|
cap = float(params.get("buy_halt_dayup") or 0.05)
|
|
|
|
|
|
if dayup is not None and float(dayup) > cap:
|
|
|
|
|
|
return out(ACT_STOP, f"当日涨幅 {float(dayup):.2%} > {cap:.0%}, 停止买入 (不追高)")
|
|
|
|
|
|
premium = round(2 - disc, 4) # 买入兜底与卖出对称: 0.998 → 1.002
|
|
|
|
|
|
if now_min >= eod_min:
|
2026-08-06 15:25:39 +08:00
|
|
|
|
# 窗口末日的强制完成**只对命令驱动生效** (2026-08-06)。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 由来: 2026-08-06 实机, 000063.SZ 那条建仓指令连日判「现价高于买入区间上沿,
|
|
|
|
|
|
# 不追」, 到窗口末日 14:46 照样按 现价×1.002 追进去 1700 股。那一笔是命令驱动的
|
|
|
|
|
|
# (用户下过「投这么多」的命令, 到期必须完成, 强制是对的), 但同一段代码等新建仓
|
|
|
|
|
|
# 上线就会作用在自主提议上 —— 系统自己挑的票, 连着三天判"不追", 第三天下午
|
|
|
|
|
|
# 无人值守地追进去, 与「可以接受买不上」那条原则直接冲突。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 更要紧的是: window_verdict 早就把口径写死了 ——
|
|
|
|
|
|
# "PARTIAL" if is_command else "EXPIRED" (自主类窗口耗尽直接作废)
|
|
|
|
|
|
# 既然自主的到期就作废, 就不该在到期当天先被强制完成一遍。这两处本来是矛盾的,
|
|
|
|
|
|
# 这次是把它们对齐, 不是新增策略。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 影响面要说破: 已有的 FILL / ADD / DCA 自主买单也跟着改 —— 它们从前也会在
|
|
|
|
|
|
# 末日强制完成, 现在到期作废。「回踩补足」在末日追高买本身就自相矛盾, 所以
|
|
|
|
|
|
# 这对它们同样是修正, 但确实是既有行为的改变。
|
|
|
|
|
|
if is_last_day and is_command:
|
2026-07-28 09:10:07 +08:00
|
|
|
|
return out(ACT_FIRE, f"窗口末日 {_fmt(eod_min)} 强制完成: 限价 = 现价×{premium}",
|
|
|
|
|
|
limit=round(price * premium, 2), forced=True)
|
2026-08-06 15:25:39 +08:00
|
|
|
|
if is_last_day:
|
|
|
|
|
|
return out(ACT_WAIT, f"窗口末日 {_fmt(eod_min)} 之后不追买 —— "
|
|
|
|
|
|
f"自主买入不做强制完成, 本条到期作废 (可以接受买不上)")
|
2026-07-28 09:10:07 +08:00
|
|
|
|
return out(ACT_WAIT, f"{_fmt(eod_min)} 后不新开买单, 顺延次日")
|
2026-08-03 12:49:01 +08:00
|
|
|
|
return None
|
2026-07-28 09:10:07 +08:00
|
|
|
|
|
|
|
|
|
|
return out(ACT_SKIP, f"方向 {side!r} 非法")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 12:49:01 +08:00
|
|
|
|
def decide(*, side: str, now, day: dict, params: dict, is_last_day: bool,
|
2026-08-06 15:25:39 +08:00
|
|
|
|
is_command: bool = True, fired_today: int = 0, quota: int = 0) -> dict:
|
2026-08-03 12:49:01 +08:00
|
|
|
|
"""实现B: 单条指令在「此刻」该不该出手 (= 硬闸 + 内置保守看法)。
|
|
|
|
|
|
|
|
|
|
|
|
day: {price, vwap, halted, limit_up, limit_down, day_chg_from_open, support}
|
|
|
|
|
|
params: {sell_avoid_open_min, buy_halt_dayup, eod_force_time, eod_force_discount}
|
|
|
|
|
|
返回 {"action", "qty_hint", "limit_price", "reason", "forced"}
|
2026-08-06 15:25:39 +08:00
|
|
|
|
|
|
|
|
|
|
is_command 默认 True (命令口径 = 2026-08-06 之前的行为), 只影响买入的窗口末日强制完成,
|
|
|
|
|
|
见 hard_gate 里那段说明。生产路径由 exec_advisor 从指令的 progress.is_command 显式传下来,
|
|
|
|
|
|
这里给默认值只是为了让既有单测与临时试算不必逐个改口径。
|
2026-08-03 12:49:01 +08:00
|
|
|
|
"""
|
|
|
|
|
|
h = hard_gate(side=side, now=now, day=day, params=params, is_last_day=is_last_day,
|
2026-08-06 15:25:39 +08:00
|
|
|
|
is_command=is_command, fired_today=fired_today, quota=quota)
|
2026-08-03 12:49:01 +08:00
|
|
|
|
if h is not None:
|
|
|
|
|
|
return h
|
|
|
|
|
|
|
|
|
|
|
|
now_min = hm_to_min(now)
|
|
|
|
|
|
price = float(day.get("price") or 0)
|
|
|
|
|
|
vwap = float(day.get("vwap") or 0)
|
|
|
|
|
|
disc = float(params.get("eod_force_discount") or 0.998)
|
|
|
|
|
|
left = max(0, int(quota) - int(fired_today))
|
|
|
|
|
|
|
|
|
|
|
|
def out(action, reason, limit=None, forced=False):
|
|
|
|
|
|
return {"action": action, "qty_hint": left, "limit_price": limit,
|
|
|
|
|
|
"reason": reason, "forced": forced}
|
|
|
|
|
|
|
|
|
|
|
|
if side == "sell":
|
|
|
|
|
|
avoid = int(params.get("sell_avoid_open_min") or 30)
|
|
|
|
|
|
if now_min < OPEN_MIN + avoid:
|
|
|
|
|
|
return out(ACT_WAIT, f"避开开盘 {avoid} 分钟 (至 {_fmt(OPEN_MIN + avoid)})")
|
|
|
|
|
|
if vwap > 0 and price >= vwap:
|
|
|
|
|
|
return out(ACT_FIRE, f"现价 {price} ≥ 当日均价 {vwap}, 分笔卖出配额",
|
|
|
|
|
|
limit=round(price * disc, 2))
|
|
|
|
|
|
return out(ACT_WAIT, f"现价 {price} < 当日均价 {vwap or '—'}, 等更好的价")
|
|
|
|
|
|
|
|
|
|
|
|
premium = round(2 - disc, 4)
|
|
|
|
|
|
support = float(day.get("support") or 0)
|
|
|
|
|
|
if vwap > 0 and price <= vwap:
|
|
|
|
|
|
return out(ACT_FIRE, f"现价 {price} ≤ 当日均价 {vwap}, 买入配额",
|
|
|
|
|
|
limit=round(price * premium, 2))
|
|
|
|
|
|
if support > 0 and price <= support * 1.01:
|
|
|
|
|
|
return out(ACT_FIRE, f"现价 {price} 进入回踩带 (支撑 {support}), 买入配额",
|
|
|
|
|
|
limit=round(price * premium, 2))
|
|
|
|
|
|
return out(ACT_WAIT, f"现价 {price} > 当日均价 {vwap or '—'} 且未回踩, 等回调")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_advice(*, side: str, day: dict, params: dict, advice: dict, left: int):
|
|
|
|
|
|
"""把决策系统的应答折算成与 decide() 同构的决策 (实现A的落地一跳)。
|
|
|
|
|
|
|
|
|
|
|
|
advice: {"verdict": FIRE|WAIT, "limit_price"?, "reason"?} —— 已过 hard_gate 才会走到这。
|
|
|
|
|
|
建议价通常是执行区间的边缘, 离现价百分之几属正常; 缺失/非法/偏离现价超过
|
|
|
|
|
|
advice_limit_band (默认 10%) 才视为异常数据, 按本地口径重定 (买 现价×premium /
|
|
|
|
|
|
卖 现价×disc) 并在 reason 里留痕 —— 限价最终要过出口表的参数校验, 离谱的建议价
|
|
|
|
|
|
与其被拒不如就地纠偏。verdict 无法识别返回 None, 由调用方退实现B。
|
|
|
|
|
|
"""
|
|
|
|
|
|
price = float(day.get("price") or 0)
|
|
|
|
|
|
disc = float(params.get("eod_force_discount") or 0.998)
|
|
|
|
|
|
premium = round(2 - disc, 4)
|
|
|
|
|
|
band = float(params.get("advice_limit_band") or 0.03)
|
|
|
|
|
|
v = str(advice.get("verdict") or "").strip().upper()
|
|
|
|
|
|
reason = f"[实现A] {advice.get('reason') or '决策系统未给理由'}"
|
|
|
|
|
|
|
|
|
|
|
|
if v == "WAIT":
|
|
|
|
|
|
return {"action": ACT_WAIT, "qty_hint": left, "limit_price": None,
|
|
|
|
|
|
"reason": reason, "forced": False}
|
|
|
|
|
|
if v == "FIRE":
|
|
|
|
|
|
fallback = round(price * (premium if side == "buy" else disc), 2)
|
|
|
|
|
|
try:
|
|
|
|
|
|
limit = float(advice.get("limit_price") or 0)
|
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
|
limit = 0.0
|
|
|
|
|
|
if limit <= 0:
|
|
|
|
|
|
limit = fallback
|
|
|
|
|
|
elif price > 0 and abs(limit / price - 1) > band:
|
|
|
|
|
|
reason += f" (建议价 {limit} 偏离现价超 {band:.0%}, 按本地口径 {fallback})"
|
|
|
|
|
|
limit = fallback
|
|
|
|
|
|
return {"action": ACT_FIRE, "qty_hint": left, "limit_price": round(limit, 2),
|
|
|
|
|
|
"reason": reason, "forced": False}
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 09:10:07 +08:00
|
|
|
|
def _fmt(m: int) -> str:
|
|
|
|
|
|
return f"{m // 60:02d}:{m % 60:02d}"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 15:48:57 +08:00
|
|
|
|
def add_trade_minutes(start_min: int, minutes: int) -> int:
|
|
|
|
|
|
"""从 start_min 起推进 N 个「有效交易分钟」, 返回当日分钟数 (收盘封顶)。
|
|
|
|
|
|
|
|
|
|
|
|
午休 11:30~13:00 不计入 —— 挂单在午休不撮合, 把这 90 分钟算进有效期
|
|
|
|
|
|
等于凭空把有效期砍掉一大截。11:25 下的单给 10 分钟, 应该活到 13:05,
|
|
|
|
|
|
而不是 11:35 就被撤掉。
|
|
|
|
|
|
"""
|
|
|
|
|
|
m = max(int(start_min), OPEN_MIN)
|
|
|
|
|
|
left = max(0, int(minutes))
|
|
|
|
|
|
if LUNCH_START < m < LUNCH_END: # 起点落在午休里, 从下午开盘算
|
|
|
|
|
|
m = LUNCH_END
|
|
|
|
|
|
if m <= LUNCH_START and m + left > LUNCH_START:
|
|
|
|
|
|
left -= (LUNCH_START - m) # 用掉上午剩余部分
|
|
|
|
|
|
m = LUNCH_END
|
|
|
|
|
|
return min(m + left, CLOSE_MIN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def slice_deadline(now, *, ttl_min: int = 10, forced: bool = False) -> int:
|
|
|
|
|
|
"""单个下发分片的有效期截止 (当日分钟数)。下游到点未成交即自动撤单。
|
|
|
|
|
|
|
|
|
|
|
|
普通分片只给 ttl_min 个交易分钟: run_tick 每分钟重评一次, 撤掉重下比挂着更好
|
|
|
|
|
|
—— 限价是按下发那一刻的价算的, 挂久了价已经不是那个价, 还白占可用资金/持仓。
|
|
|
|
|
|
兜底单 (14:45 之后的 forced) 直接给到收盘: 那是「今天必须走掉」的单, 不能被
|
|
|
|
|
|
TTL 撤回来。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if forced:
|
|
|
|
|
|
return CLOSE_MIN
|
|
|
|
|
|
return add_trade_minutes(hm_to_min(now), ttl_min)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 09:10:07 +08:00
|
|
|
|
def window_verdict(*, remaining_qty: int, tdays_left: int, is_command: bool) -> dict:
|
|
|
|
|
|
"""窗口耗尽时的收口 (设计 §8 末句)。
|
|
|
|
|
|
|
|
|
|
|
|
还有剩余且窗口已尽 → 命令置「部分完成」并告警; 命令类保留人工兜底提示,
|
|
|
|
|
|
自主类直接作废。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if int(remaining_qty or 0) <= 0:
|
|
|
|
|
|
return {"verdict": "DONE", "note": "已足额完成"}
|
|
|
|
|
|
if int(tdays_left or 0) > 0:
|
|
|
|
|
|
return {"verdict": "RUNNING", "note": f"窗口内剩余 {tdays_left} 交易日"}
|
|
|
|
|
|
return {"verdict": "PARTIAL" if is_command else "EXPIRED",
|
|
|
|
|
|
"note": f"窗口耗尽仍剩 {remaining_qty} 股 —— "
|
|
|
|
|
|
+ ("命令置部分完成并告警, 请在页面决定顺延或人工完成"
|
|
|
|
|
|
if is_command else "自主指令作废")}
|