盘中确认包:给技术面这一票加盘中放量确认,只用于入场(台账012)

方案第三节的盘中确认,之前没落地——技术面那一票全是昨夜数据。现在加两条规则,
只用于入场,时段与量比与解锁重问同源(台账003):
- 收口突破:收口等待的候选盘中站上昨日布林上轨且时段折算量比≥1.5,当轮视为开口向上,
  改立场重新合议后进候选。
- 开口确认:开口向上的候选盘中要现价不低于当日均价且量比≥1.5,不满足就当轮进观察,
  处置词「等盘中确认」,取不到按等待不放行(入场的门,方向保守)。

做了什么:
- core/tech_rules.py 加纯函数 intraday_confirm;synthesize 的 base 带 boll_upper(收口突破
  拿昨日上轨);时段判断复用 reask_rules.in_window(同一函数对象,台账003同源哨兵)。
- tech_service._compact 带 boll_upper。
- proposal_service._attach_consensus 装配后跑盘中确认:突破改 blocks 重新合议、开口未确认
  把路由改观察带 wait_confirm、硬数字 tech 块记 intraday;_scan_open 与 _attach_consensus
  多接 now。动作引擎 scan_open 观察分支按 con 的 disp 选处置词与标签。
- 处置词 wait_confirm(页面「等盘中确认」,与在盯同档);观察读数新增 open_wait_confirm、
  open_breakout 两类。
- 四参数登记(PMS_TECH_INTRADAY_CONFIRM/BREAKOUT/VOL_CONFIRM/WINDOW),量比阈值加范围。

拍板点二(开口向上候选在时段外进观察)按规格书附录乙实现,事后确认即可。
总闸 PMS_TECH_INTRADAY_CONFIRM 关掉两条规则都不走,逐字回旧。第三十二批19例,
开发机全量866例 ALL SUITES PASS。未推未部署(新包改入场行为,部署时机待定)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-14 11:40:51 +08:00
parent 94da25bc25
commit 1ccd542898
9 changed files with 417 additions and 15 deletions

View File

@ -51,6 +51,9 @@ BUY, SELL = "buy", "sell"
# 候选处置词 (2026-09-11 三源合议工作包二): 合议判「观察」的新建仓候选写这个 disp, # 候选处置词 (2026-09-11 三源合议工作包二): 合议判「观察」的新建仓候选写这个 disp,
# 页面把它渲染成「等技术面开口」。与既有的 would / deny 并列, 走 disposition_snapshot 那条路。 # 页面把它渲染成「等技术面开口」。与既有的 would / deny 并列, 走 disposition_snapshot 那条路。
DISP_WAIT_TECH = "wait_tech" DISP_WAIT_TECH = "wait_tech"
# 盘中确认包 (2026-09-14): 开口向上未确认 / 收口等待时段外 的候选进观察, 处置词写这个,
# 页面渲染成「等盘中确认」。与 wait_tech 并列, 走 disposition_snapshot 同一条路。
DISP_WAIT_CONFIRM = "wait_confirm"
# 观察读数包 (2026-09-14, 台账 013): 合议相关跳过项打的类别标签, 值即 pms_consensus_stat.kind。 # 观察读数包 (2026-09-14, 台账 013): 合议相关跳过项打的类别标签, 值即 pms_consensus_stat.kind。
# consensus_stats.record_round 认这些标签落表。**只在合议开着 (con 非 None) 或增持门开着 # consensus_stats.record_round 认这些标签落表。**只在合议开着 (con 非 None) 或增持门开着
@ -59,7 +62,10 @@ DISP_WAIT_TECH = "wait_tech"
TAG_OPEN_SKIP_FUND = "open_skip_fund" # 合议判跳过, 因没有买方评析 TAG_OPEN_SKIP_FUND = "open_skip_fund" # 合议判跳过, 因没有买方评析
TAG_OPEN_SKIP_BEAR = "open_skip_bear" # 合议判跳过, 其余原因 (方向看空) TAG_OPEN_SKIP_BEAR = "open_skip_bear" # 合议判跳过, 其余原因 (方向看空)
TAG_OPEN_WAIT_TECH = "open_wait_tech" # 合议判观察 (等技术面开口/转向) TAG_OPEN_WAIT_TECH = "open_wait_tech" # 合议判观察 (等技术面开口/转向)
TAG_OPEN_WAIT_CONFIRM = "open_wait_confirm" # 盘中确认包: 开口未确认/时段外, 等盘中确认
TAG_GATE_BLOCK = "gate_block" # 持仓增持门拦下 (detail 里带 gate_kind: fill/add/dca) TAG_GATE_BLOCK = "gate_block" # 持仓增持门拦下 (detail 里带 gate_kind: fill/add/dca)
# open_breakout (盘中收口突破视为开口) 不打在跳过项上 —— 突破的候选会被放行、不进 skipped,
# 由 proposal_service 记进 consensus_seen 的 intraday 字段, consensus_stats 据此落表。
# 同一轮里出现多条减持时留哪一条 (2026-09-03): 数字小的优先。用户自己设的目标价到价排在 # 同一轮里出现多条减持时留哪一条 (2026-09-03): 数字小的优先。用户自己设的目标价到价排在
# 系统按规则算出来的保垫减仓前面 —— 人已经说了到价就清, 这一轮就不该再自作主张先卖一部分。 # 系统按规则算出来的保垫减仓前面 —— 人已经说了到价就清, 这一轮就不该再自作主张先卖一部分。
@ -930,9 +936,13 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
"tag": TAG_OPEN_SKIP_FUND if "买方评析" in _rr else TAG_OPEN_SKIP_BEAR}) "tag": TAG_OPEN_SKIP_FUND if "买方评析" in _rr else TAG_OPEN_SKIP_BEAR})
continue continue
if con and con.get("route") == "观察": if con and con.get("route") == "观察":
skipped.append({"ts_code": code, "action": A_OPEN, "disp": DISP_WAIT_TECH, # 处置词: con 带 disp 就用它 (盘中确认包给 opened_wait/outside_window 写 wait_confirm),
# 否则用 wait_tech (等技术面开口)。tag 随之分 open_wait_confirm / open_wait_tech。
_disp = con.get("disp") or DISP_WAIT_TECH
_tag = TAG_OPEN_WAIT_CONFIRM if _disp == DISP_WAIT_CONFIRM else TAG_OPEN_WAIT_TECH
skipped.append({"ts_code": code, "action": A_OPEN, "disp": _disp,
"why": con.get("route_reason") or "三源合议判为观察,等技术面开口", "why": con.get("route_reason") or "三源合议判为观察,等技术面开口",
"tag": TAG_OPEN_WAIT_TECH}) "tag": _tag})
continue continue
if slots <= 0: if slots <= 0:
skipped.append({"ts_code": code, "action": A_OPEN, skipped.append({"ts_code": code, "action": A_OPEN,

View File

@ -17,6 +17,11 @@ ddl_pms_v1.sql 的 pms_tech_daily), 最后一行是最新那天。数据新鲜
""" """
from __future__ import annotations from __future__ import annotations
# 盘中确认包 (2026-09-14): 时段判断复用解锁重问那一份 in_window, 不另写 —— 台账 003
# 「三处同源」的哨兵 (解锁重问、择时、盘中确认共用同一套时段与量比算法)。量比由调用方算,
# 也走 reask_rules.vol_ratio。这里只导入 in_window, 是同一个函数对象。
from app.core.reask_rules import in_window
# 数翻向的窗口「最近 20 个交易日」(方案第三节)。窗口本身不设页面参数, 翻向次数阈值才设。 # 数翻向的窗口「最近 20 个交易日」(方案第三节)。窗口本身不设页面参数, 翻向次数阈值才设。
CHOPPY_WINDOW = 20 CHOPPY_WINDOW = 20
@ -101,6 +106,7 @@ def synthesize(rows, *, params=None) -> dict:
sar_value = _num(latest.get("sar_value")) sar_value = _num(latest.get("sar_value"))
base = {"sar_side": side, "sar_value": sar_value, "sar_flip_days": flip_days, base = {"sar_side": side, "sar_value": sar_value, "sar_flip_days": flip_days,
"boll_upper": _num(latest.get("boll_upper")), # 盘中确认包: 收口突破要拿昨日上轨
"data_date": latest.get("data_date"), "no_read_why": None} "data_date": latest.get("data_date"), "no_read_why": None}
# ---- 第二步: 震荡市 ---- # ---- 第二步: 震荡市 ----
@ -156,3 +162,54 @@ def synthesize(rows, *, params=None) -> dict:
return out("中性", None, "震荡", f"近 20 日 SAR 翻向 {flips} 次,震荡市不表态") return out("中性", None, "震荡", f"近 20 日 SAR 翻向 {flips} 次,震荡市不表态")
# 兜底: side 已限定在 {多, 空}, 上面各分支理应已覆盖全部组合; 到这里保守中性。 # 兜底: side 已限定在 {多, 空}, 上面各分支理应已覆盖全部组合; 到这里保守中性。
return out("中性", None, "未归类", "指标组合未归入任何相位,保守中性") return out("中性", None, "未归类", "指标组合未归入任何相位,保守中性")
def intraday_confirm(state, *, price, vwap, upper_prev, vol_ratio, now,
window="0945-1430", vol_min=1.5, breakout_on=True) -> dict:
"""给技术面这一票加盘中确认, **只用于入场** (方案第三节、附录乙)。纯逻辑, 零外部依赖。
state synthesize 出的技术面块 (至少含 phase)返回:
{"state": 新块或原块, "changed": bool,
"kind": None | "breakout" | "opened_ok" | "opened_wait" | "outside_window",
"why": str, "reads": {price, vwap, upper_prev, vol_ratio, at}}
判定次序 (附录乙):
1 相位不是收口等待也不是开口向上 原样返回, kind
2 不在时段内: 开口向上 outside_window (进观察等确认); 收口等待 原样返回
3 收口等待: breakout_on 现价/昨日上轨/量比都有值 现价>上轨 量比vol_min
breakout, 立场看多相位盘中开口向上; 任一不满足原样返回
4 开口向上: 现价/均价/量比都有值 现价均价 量比vol_min opened_ok (块不变);
任一取不到或不满足 opened_wait**取不到按等待而不是放行** 入场的门, 方向保守"""
st = state if isinstance(state, dict) else {}
phase = st.get("phase")
at = now.strftime("%H:%M") if now is not None else None
reads = {"price": price, "vwap": vwap, "upper_prev": upper_prev,
"vol_ratio": vol_ratio, "at": at}
def result(new_state, changed, kind, why):
return {"state": new_state, "changed": changed, "kind": kind, "why": why, "reads": reads}
if phase not in ("收口等待", "开口向上"):
return result(st, False, None, "")
if now is None or not in_window(now, window):
if phase == "开口向上":
return result(st, False, "outside_window",
"开口向上候选不在确认时段 %s 内,进观察等确认" % window)
return result(st, False, None, "") # 收口等待在时段外照旧观察 (原样)
if phase == "收口等待":
if (breakout_on and price is not None and upper_prev is not None
and vol_ratio is not None and price > upper_prev and vol_ratio >= vol_min):
new = dict(st)
new.update({"stance": "看多", "strength": "", "phase": "盘中开口向上",
"reason": "盘中站上昨日布林上轨且放量:收口突破视为开口向上"})
return result(new, True, "breakout",
"现价 %s 站上昨日上轨 %s、量比 %s%s:收口突破视为开口向上"
% (price, upper_prev, vol_ratio, vol_min))
return result(st, False, None, "收口等待,盘中未突破")
# phase == 开口向上, 在时段内
if (price is not None and vwap is not None and vol_ratio is not None
and price >= vwap and vol_ratio >= vol_min):
return result(st, False, "opened_ok",
"现价 %s≥当日均价 %s、量比 %s%s:开口确认" % (price, vwap, vol_ratio, vol_min))
return result(st, False, "opened_wait",
"开口向上未确认(现价未站上均价或量能不足/取不到),进观察等确认")

View File

@ -42,10 +42,12 @@ STAT_TIMES_KEY = "PMS_CONSENSUS_STAT_TIMES"
K_OPEN_SKIP_FUND = ae.TAG_OPEN_SKIP_FUND K_OPEN_SKIP_FUND = ae.TAG_OPEN_SKIP_FUND
K_OPEN_SKIP_BEAR = ae.TAG_OPEN_SKIP_BEAR K_OPEN_SKIP_BEAR = ae.TAG_OPEN_SKIP_BEAR
K_OPEN_WAIT_TECH = ae.TAG_OPEN_WAIT_TECH K_OPEN_WAIT_TECH = ae.TAG_OPEN_WAIT_TECH
K_OPEN_WAIT_CONFIRM = ae.TAG_OPEN_WAIT_CONFIRM # 盘中确认包: 开口未确认/时段外 (跳过项 tag)
K_GATE_BLOCK = ae.TAG_GATE_BLOCK K_GATE_BLOCK = ae.TAG_GATE_BLOCK
# 从 consensus_seen / scanned / 映射重建派生的类别 (动作引擎不打这些 tag)。 # 从 consensus_seen / scanned / 映射重建派生的类别 (动作引擎不打这些 tag)。
K_OPEN_CONFIRM = "open_confirm" K_OPEN_CONFIRM = "open_confirm"
K_OPEN_PASS = "open_pass" K_OPEN_PASS = "open_pass"
K_OPEN_BREAKOUT = "open_breakout" # 盘中确认包: 收口突破视为开口 (从 consensus_seen 的 intraday 派生)
K_FUND_NOREAD = "fund_noread" K_FUND_NOREAD = "fund_noread"
K_TIMING_NOREAD = "timing_noread" K_TIMING_NOREAD = "timing_noread"
K_TECH_EXIT = "tech_exit" K_TECH_EXIT = "tech_exit"
@ -120,6 +122,9 @@ def _seen_rows(out, day, now) -> list:
rows.append(_row(day, code, K_FUND_NOREAD, reason="基本面无读数", now=now)) rows.append(_row(day, code, K_FUND_NOREAD, reason="基本面无读数", now=now))
if s.get("timing") == "无读数": if s.get("timing") == "无读数":
rows.append(_row(day, code, K_TIMING_NOREAD, reason="择时无读数", now=now)) rows.append(_row(day, code, K_TIMING_NOREAD, reason="择时无读数", now=now))
if s.get("intraday") == "breakout": # 盘中确认包: 收口突破视为开口
rows.append(_row(day, code, K_OPEN_BREAKOUT, reason="盘中收口突破视为开口",
detail=detail, now=now))
return rows return rows

View File

@ -132,6 +132,12 @@ RUNTIME_EXTRA = {
# 四位时刻逗号分隔; consensus_stats.record_round 只在这些时刻及其下一分钟写路由与装配两类 # 四位时刻逗号分隔; consensus_stats.record_round 只在这些时刻及其下一分钟写路由与装配两类
# (一天最多四次), 转空离场实弹随时写。设为空串即不写检查点行, 只留转空实弹。 # (一天最多四次), 转空离场实弹随时写。设为空串即不写检查点行, 只留转空实弹。
"PMS_CONSENSUS_STAT_TIMES": ("0935,1030,1330,1445", str, "合议观察读数落表的检查点 (四位时刻逗号分隔; 空串=只记转空实弹)"), "PMS_CONSENSUS_STAT_TIMES": ("0935,1030,1330,1445", str, "合议观察读数落表的检查点 (四位时刻逗号分隔; 空串=只记转空实弹)"),
# ── 2026-09-14 盘中确认包 (方案第三节, 台账 012): 给技术面这一票加盘中确认, 只用于入场。
# 时段与量比与解锁重问同源 (台账 003)。总闸关掉两条规则都不走, 行为逐字如旧。
"PMS_TECH_INTRADAY_CONFIRM": (True, bool, "盘中确认总闸 (关掉两条规则都不走, 入场行为逐字如旧)"),
"PMS_TECH_INTRADAY_BREAKOUT": (True, bool, "收口突破规则开关 (关掉只剩开口确认规则)"),
"PMS_TECH_VOL_CONFIRM": (1.5, float, "盘中确认的时段折算量比阈值 (与解锁重问 PMS_OPEN_REASK_VOL_MIN 同值同义)"),
"PMS_TECH_INTRADAY_WINDOW": ("0945-1430", str, "盘中确认时段 (与解锁重问 PMS_OPEN_REASK_WINDOW 同义; 时段外开口向上候选进观察等确认)"),
} }
# **读不到时必须按"已暂停"处理的键 (fail-closed)。** # **读不到时必须按"已暂停"处理的键 (fail-closed)。**
@ -512,6 +518,7 @@ _RANGES = {
"PMS_FUND_STALE_DAYS": (0, 3650), "PMS_FUND_CONSENSUS_GOOD_MIN": (0, 10), "PMS_FUND_STALE_DAYS": (0, 3650), "PMS_FUND_CONSENSUS_GOOD_MIN": (0, 10),
"PMS_TECH_EXIT_TRIM_RATIO": (0, 1), "PMS_TECH_SAR_STOP_BUFFER": (0, 0.1), "PMS_TECH_EXIT_TRIM_RATIO": (0, 1), "PMS_TECH_SAR_STOP_BUFFER": (0, 0.1),
"PMS_TECH_TIGHT_TRAIL_GIVEBACK": (0.005, 0.5), "PMS_TECH_TIGHT_TRAIL_TARGET": (0.01, 1), "PMS_TECH_TIGHT_TRAIL_GIVEBACK": (0.005, 0.5), "PMS_TECH_TIGHT_TRAIL_TARGET": (0.01, 1),
"PMS_TECH_VOL_CONFIRM": (0.5, 10), # 盘中确认量比阈值 (与 PMS_OPEN_REASK_VOL_MIN 同范围)
# 宏观择时层 # 宏观择时层
"PMS_MACRO_HOT_TH": (0, 100), "PMS_MACRO_COLD_TH": (-100, 0), "PMS_MACRO_HOT_TH": (0, 100), "PMS_MACRO_COLD_TH": (-100, 0),
"PMS_MACRO_EXIT_BAND": (0, 100), "PMS_MACRO_CONFIRM_DAYS": (1, 10), "PMS_MACRO_EXIT_BAND": (0, 100), "PMS_MACRO_CONFIRM_DAYS": (1, 10),

View File

@ -33,10 +33,12 @@ import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from app.core import action_engine as ae from app.core import action_engine as ae
from app.core import consensus
from app.core import copy from app.core import copy
from app.core import command_spec as cs from app.core import command_spec as cs
from app.core import rule_gate from app.core import rule_gate
from app.core import signal_rules as sr from app.core import signal_rules as sr
from app.core import tech_rules
from app.core import tradedays as td from app.core import tradedays as td
from app.repo import pms_repo from app.repo import pms_repo
from app.services import (candidate_pub, command_service, consensus_service, executor, industry, from app.services import (candidate_pub, command_service, consensus_service, executor, industry,
@ -188,7 +190,7 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
out["skipped"].append({"action": ae.A_OPEN, "why": "新建仓档位 off, 不扫描候选池"}) out["skipped"].append({"action": ae.A_OPEN, "why": "新建仓档位 off, 不扫描候选池"})
else: else:
try: try:
open_cands = _scan_open(view, params, stock_params, skip, mkt, out) open_cands = _scan_open(view, params, stock_params, skip, mkt, out, now=now)
except Exception as e: except Exception as e:
logger.exception("新建仓扫描失败") logger.exception("新建仓扫描失败")
out["errors"].append(f"新建仓扫描失败: {type(e).__name__}: {e}") out["errors"].append(f"新建仓扫描失败: {type(e).__name__}: {e}")
@ -283,7 +285,7 @@ def _save_tech_exit_done(codes) -> None:
# ================================================================ 新建仓的取数与筛选 # ================================================================ 新建仓的取数与筛选
def _scan_open(view, params, stock_params, skip, mkt, out) -> list: def _scan_open(view, params, stock_params, skip, mkt, out, now=None) -> list:
"""候选池 → 新建仓候选。产出的候选数天生不超过剩余名额 (名额与金额在纯逻辑里边走边扣)。 """候选池 → 新建仓候选。产出的候选数天生不超过剩余名额 (名额与金额在纯逻辑里边走边扣)。
与命令驱动那条路 (command_service._candidates) 有意不同的两点: 与命令驱动那条路 (command_service._candidates) 有意不同的两点:
@ -384,7 +386,7 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
# 开关关掉整段不做, cands 原样进 scan_open —— 与接入前逐字相同。装配失败只记日志、不拦扫描 # 开关关掉整段不做, cands 原样进 scan_open —— 与接入前逐字相同。装配失败只记日志、不拦扫描
# (合议是加不改: 装不上就当没有合议, scan_open 里 con 为 None 自然退回旧路)。 # (合议是加不改: 装不上就当没有合议, scan_open 里 con 为 None 自然退回旧路)。
if params.get("consensus_route"): if params.get("consensus_route"):
_attach_consensus(cands, out) _attach_consensus(cands, out, now=now)
res = ae.scan_open(candidates=cands, params=params, caps=portfolio.caps_ctx(view), res = ae.scan_open(candidates=cands, params=params, caps=portfolio.caps_ctx(view),
room_amt=room, slots=slots, skip=skip) room_amt=room, slots=slots, skip=skip)
out["skipped"].extend(res["skipped"]) out["skipped"].extend(res["skipped"])
@ -403,7 +405,73 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
return res["candidates"] return res["candidates"]
def _attach_consensus(cands, out=None) -> None: def _intraday_cfg() -> dict | None:
"""盘中确认包的开关与阈值 (2026-09-14, 台账 012)。总闸关掉返回 None —— 整段不走, 逐字回旧。
cparams 是合议参数, 收口突破改立场后要用它重新合议"""
if not param_store.get_bool("PMS_TECH_INTRADAY_CONFIRM", True):
return None
return {"breakout_on": param_store.get_bool("PMS_TECH_INTRADAY_BREAKOUT", True),
"vol_min": param_store.get_float("PMS_TECH_VOL_CONFIRM", 1.5),
"window": param_store.get("PMS_TECH_INTRADAY_WINDOW", "0945-1430") or "0945-1430",
"cparams": consensus_service._params()}
def _intraday_market(code, cache) -> tuple:
"""一只票的盘中读数 (现价/均价/时段折算量比)。本轮按代码缓存, 同一只不取两次。
量比走与解锁重问相同的算法 (reask_service._vol_ratio)任一取数失败传 None (按取不到)"""
if code in cache:
return cache[code]
price = vwap = vratio = None
try:
day = market.day_snapshot(code) or {}
price, vwap = day.get("price"), day.get("vwap")
except Exception as e: # noqa: BLE001
logger.warning("[盘中确认] 取当日快照失败 %s: %s", code, e)
try:
vratio = reask_service._vol_ratio(code)
except Exception as e: # noqa: BLE001
logger.warning("[盘中确认] 取量比失败 %s: %s", code, e)
cache[code] = (price, vwap, vratio)
return cache[code]
def _apply_intraday(code, blocks, cfg, now, cache) -> str | None:
"""对一只候选跑盘中确认, 原地改 blocks。返回 intraday kind (或 None, 表示相位无关未跑)。
收口突破改技术面立场并重新合议; 开口未确认/时段外把路由改观察带 wait_confirm; 都在硬数字 tech 块记 intraday"""
tech = blocks.get("tech") or {}
if tech.get("phase") not in ("收口等待", "开口向上"):
return None
price, vwap, vratio = _intraday_market(code, cache)
r = tech_rules.intraday_confirm(tech, price=price, vwap=vwap,
upper_prev=tech.get("boll_upper"), vol_ratio=vratio, now=now,
window=cfg["window"], vol_min=cfg["vol_min"],
breakout_on=cfg["breakout_on"])
kind = r.get("kind")
if kind == "breakout":
new_tech = r["state"]
blocks["tech"] = new_tech
cp = cfg["cparams"]
blocks["consensus"] = consensus.decide(
(blocks.get("fund") or {}).get("stance") or "无读数",
new_tech.get("stance") or "无读数",
(blocks.get("timing") or {}).get("stance") or "无读数",
tech_phase=new_tech.get("phase"),
fund_required=(cp.get("fund_required") and cp.get("consensus_route")),
weak_confirm=cp.get("weak_confirm", True))
new_tech["intraday"] = {"kind": kind, "reads": r["reads"]}
elif kind in ("opened_wait", "outside_window"):
con = blocks.get("consensus") or {}
con["route"] = "观察"
con["route_reason"] = r["why"]
con["disp"] = ae.DISP_WAIT_CONFIRM
blocks["consensus"] = con
tech["intraday"] = {"kind": kind, "reads": r["reads"]}
elif kind == "opened_ok":
tech["intraday"] = {"kind": kind, "reads": r["reads"]}
return kind
def _attach_consensus(cands, out=None, now=None) -> None:
"""给每只候选装配三源合议, 挂三样到候选上 (原地改): """给每只候选装配三源合议, 挂三样到候选上 (原地改):
consensus 合议块 (direction / votes / strength / reason / route / route_reason), scan_open 分流读它; consensus 合议块 (direction / votes / strength / reason / route / route_reason), scan_open 分流读它;
consensus_blocks 四块意见 (fund / tech / timing / consensus), scan_open 定档时取 fund/tech advise_v2; consensus_blocks 四块意见 (fund / tech / timing / consensus), scan_open 定档时取 fund/tech advise_v2;
@ -411,8 +479,13 @@ def _attach_consensus(cands, out=None) -> None:
批量取一次昨夜定性与技术面映射; 盘中转多留痕从候选自带的 sig_buy 取时刻任何一步失败都不抛 批量取一次昨夜定性与技术面映射; 盘中转多留痕从候选自带的 sig_buy 取时刻任何一步失败都不抛
装不上就当这只票没有合议 (scan_open con None 自然退回旧路), 合议是加不改 装不上就当这只票没有合议 (scan_open con None 自然退回旧路), 合议是加不改
每装配一只候选往 out["consensus_seen"] 追加一条紧凑记录 (观察读数包用, 也给候选处置快照用): 盘中确认 (2026-09-14 盘中确认包): 装配完四块后, 若总闸开着且传了 now, 对收口等待/开口向上的
ts_code / fund / tech / timing / direction / route / phaseout None 或缺该键时只装配不留痕""" 候选跑一遍 intraday_confirm 收口突破改立场重新合议, 开口未确认/时段外把路由改观察带 wait_confirm
总闸关掉 (_intraday_cfg None) 或没传 now 时整段不走, 与接入前逐字相同
每装配一只候选往 out["consensus_seen"] 追加一条紧凑记录 (观察读数/候选处置快照用):
ts_code / fund / tech / timing / direction / route / phase, 盘中突破的另记 intraday=breakout
out None 或缺该键时只装配不留痕"""
if not cands: if not cands:
return return
seen = out.setdefault("consensus_seen", []) if isinstance(out, dict) else None seen = out.setdefault("consensus_seen", []) if isinstance(out, dict) else None
@ -423,6 +496,8 @@ def _attach_consensus(cands, out=None) -> None:
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
logger.warning("[合议] 批量取数失败, 本轮候选不装配合议: %s", e) logger.warning("[合议] 批量取数失败, 本轮候选不装配合议: %s", e)
return return
icfg = _intraday_cfg() if now is not None else None
icache: dict = {}
for c in cands: for c in cands:
code = cs.normalize_code(c.get("ts_code") or "") code = cs.normalize_code(c.get("ts_code") or "")
if not code: if not code:
@ -431,16 +506,20 @@ def _attach_consensus(cands, out=None) -> None:
flip_at = (c.get("sig_buy") or {}).get("at") or None flip_at = (c.get("sig_buy") or {}).get("at") or None
blocks = consensus_service.assemble( blocks = consensus_service.assemble(
c, nightly=nightly.get(code), tech_states=tech_states, flip_at=flip_at) c, nightly=nightly.get(code), tech_states=tech_states, flip_at=flip_at)
ikind = _apply_intraday(code, blocks, icfg, now, icache) if icfg else None
c["consensus"] = blocks["consensus"] c["consensus"] = blocks["consensus"]
c["consensus_blocks"] = blocks c["consensus_blocks"] = blocks
c["consensus_hard"] = consensus_service.hard_keys(blocks) c["consensus_hard"] = consensus_service.hard_keys(blocks)
if seen is not None: if seen is not None:
con, fund, tech, tm = (blocks["consensus"], blocks["fund"], con, fund, tech, tm = (blocks["consensus"], blocks["fund"],
blocks["tech"], blocks["timing"]) blocks["tech"], blocks["timing"])
seen.append({"ts_code": code, "fund": fund.get("stance"), rec = {"ts_code": code, "fund": fund.get("stance"),
"tech": tech.get("stance"), "timing": tm.get("stance"), "tech": tech.get("stance"), "timing": tm.get("stance"),
"direction": con.get("direction"), "route": con.get("route"), "direction": con.get("direction"), "route": con.get("route"),
"phase": tech.get("phase")}) "phase": tech.get("phase")}
if ikind:
rec["intraday"] = ikind
seen.append(rec)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
logger.warning("[合议] 装配失败 %s (本票按无合议): %s", code, e) logger.warning("[合议] 装配失败 %s (本票按无合议): %s", code, e)
@ -612,7 +691,7 @@ def disposition_snapshot(now=None) -> dict:
for _k in _inflight_keys(): # 在途; 前端多半已按指令/提议标成「已下单/待你确认」 for _k in _inflight_keys(): # 在途; 前端多半已按指令/提议标成「已下单/待你确认」
skip[_k] = "系统已经在处理这只票(见『等我拍板』或『今日在办』)" skip[_k] = "系统已经在处理这只票(见『等我拍板』或『今日在办』)"
sink = {"skipped": []} # 丢弃用: _scan_open 只往里 append/extend, 不读它 sink = {"skipped": []} # 丢弃用: _scan_open 只往里 append/extend, 不读它
would = _scan_open(view, params, stock_params, skip, {}, sink) would = _scan_open(view, params, stock_params, skip, {}, sink, now=now)
except Exception as e: except Exception as e:
logger.exception("候选处置快照失败") logger.exception("候选处置快照失败")
return {"ok": False, "ready": False, "error": f"{type(e).__name__}: {e}", return {"ok": False, "ready": False, "error": f"{type(e).__name__}: {e}",

View File

@ -274,6 +274,7 @@ def _compact(st: dict) -> dict:
"phase": st.get("phase"), "confirm": st.get("confirm"), "phase": st.get("phase"), "confirm": st.get("confirm"),
"sar_side": st.get("sar_side"), "sar_value": st.get("sar_value"), "sar_side": st.get("sar_side"), "sar_value": st.get("sar_value"),
"sar_flip_days": st.get("sar_flip_days"), "choppy": st.get("choppy"), "sar_flip_days": st.get("sar_flip_days"), "choppy": st.get("choppy"),
"boll_upper": st.get("boll_upper"), # 盘中确认包: 收口突破从映射取昨日上轨, 不回库
"reason": st.get("reason"), "data_date": st.get("data_date")} "reason": st.get("reason"), "data_date": st.get("data_date")}

View File

@ -2882,6 +2882,8 @@ createApp({
// 三源合议判观察 (2026-09-11): 等技术面开口, 是「在盯」不是「被否决」—— // 三源合议判观察 (2026-09-11): 等技术面开口, 是「在盯」不是「被否决」——
// 归到 watch 那一档而不是今天没建仓那一档, 因为它还会回来。 // 归到 watch 那一档而不是今天没建仓那一档, 因为它还会回来。
if (d.disp === 'wait_tech') return { cls: 'wait', label: '等技术面开口', why: d.why }; if (d.disp === 'wait_tech') return { cls: 'wait', label: '等技术面开口', why: d.why };
// 盘中确认包 (2026-09-14): 开口未确认/时段外, 等盘中放量确认, 同样是「在盯」不是「被否决」。
if (d.disp === 'wait_confirm') return { cls: 'wait', label: '等盘中确认', why: d.why };
return { cls: 'deny', label: '被否决', why: d.why }; return { cls: 'deny', label: '被否决', why: d.why };
} }
return { cls: 'wait', label: '在盯', why: '' }; return { cls: 'wait', label: '在盯', why: '' };

View File

@ -93,16 +93,21 @@
转空实弹随时写试算不写被闸拒不记confirm 据动作名空串检查点)/ 转空实弹随时写试算不写被闸拒不记confirm 据动作名空串检查点)/
record_map_cover 记覆盖与逐只无读数/日报小节失败不拖垮/upsert 更新子句 record_map_cover 记覆盖与逐只无读数/日报小节失败不拖垮/upsert 更新子句
只用 VALUES 列不带绑定参数且单表合规/参数登记与检查点窗口/映射重建先清当日无读数 (21 ) 只用 VALUES 列不带绑定参数且单表合规/参数登记与检查点窗口/映射重建先清当日无读数 (21 )
test_batch32_units.py 盘中确认包 (2026-09-14 台账 012): intraday_confirm 纯函数 (相位无关原样/
收口突破要价与量同时满足/突破开关关不升/上轨缺不升/开口确认要均价与量/
取不到按等待/时段外开口进观察)/in_window reask_rules 同源哨兵/synthesize
_compact boll_upper/接线突破重合议为放行且硬数字带 intraday/开口未确认
路由改观察带 wait_confirm/总闸关掉 _intraday_cfg None/参数登记与范围 (19 )
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许 test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象 直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 ) 打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 )
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) + test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) +
目标价到价必定入队 (档位 full 也不自动卖) + 目标价到价必定入队 (档位 full 也不自动卖) +
用户设的止损价与目标价单独成列显示 (70 ) 用户设的止损价与目标价单独成列显示 (70 )
847 866
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条 (总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
减持与研究理由两键各一例, 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 654; 又加了页面文案守卫一例, 655; 09-07 审查修复加了跨轮减持等五例, 660; 第二件低把握驳回交人一例, 661; 第三件逻辑状态接入第二十二批十五例, 676; 第四件安全边际整句透传一例, 677; 参考目标价一例, 678; 催化事件与定价状态透传一例, 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例到 820; 减持与研究理由两键各一例, 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 654; 又加了页面文案守卫一例, 655; 09-07 审查修复加了跨轮减持等五例, 660; 第二件低把握驳回交人一例, 661; 第三件逻辑状态接入第二十二批十五例, 676; 第四件安全边际整句透传一例, 677; 参考目标价一例, 678; 催化事件与定价状态透传一例, 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例到 820;
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例第三十批加三例到 846; 评审修订第三十一批加一例, 现为 847) 09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例第三十批加三例到 846; 评审修订第三十一批加一例847; 09-14 盘中确认包第三十二批十九例, 现为 866)
任一子集失败即整体失败 (退出码 1) 任一子集失败即整体失败 (退出码 1)
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了): 哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
@ -157,6 +162,7 @@ SUITES = ["test_core_units.py", "test_batch2_units.py", "test_batch3_units.py",
"test_batch29_units.py", "test_batch29_units.py",
"test_batch30_units.py", "test_batch30_units.py",
"test_batch31_units.py", "test_batch31_units.py",
"test_batch32_units.py",
"test_page_enum_guard.py", "test_page_enum_guard.py",
"test_page_wiring_guard.py", "test_page_wiring_guard.py",
"test_wiring.py"] "test_wiring.py"]

View File

@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
"""盘中确认包 (2026-09-14 方案第三节, 台账 012)。全部离线, 不连库。
给技术面这一票加盘中确认, 只用于入场时段与量比与解锁重问同源 (台账 003)
A intraday_confirm 纯函数: 相位无关原样/收口突破要价与量同时满足/突破开关关不升/取不到按等待/
开口确认要均价与量/时段外开口进观察
B 同源哨兵与映射带上轨: in_window reask_rules 那一个; synthesize base _compact boll_upper
C 接线 (proposal_service._apply_intraday): 突破改立场重合议为放行硬数字带 intraday;
开口未确认路由改观察带 wait_confirm; 总闸关掉 _intraday_cfg None (逐字回旧)
E 参数登记与范围
"""
import os
import sys
import traceback
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.core import tech_rules as tr # noqa: E402
from app.core import reask_rules as rr # noqa: E402
from app.services import param_store as ps # noqa: E402
RESULTS = []
def case(name):
def deco(fn):
RESULTS.append((name, fn))
return fn
return deco
def _st(phase, **kw):
"""一个技术面立场块 (synthesize 的紧凑态)。"""
d = {"stance": "中性", "strength": None, "phase": phase, "sar_side": "",
"boll_upper": 10.0, "reason": "x"}
d.update(kw)
return d
NOW = datetime(2026, 9, 15, 10, 0) # 在 0945-1430 时段内
# ================================================================ A intraday_confirm 纯函数
@case("A 相位无关 (趋势多) → 原样返回, kind 空, 不改")
def _():
r = tr.intraday_confirm(_st("趋势多"), price=11, vwap=10, upper_prev=10,
vol_ratio=3, now=NOW)
assert r["kind"] is None and r["changed"] is False
@case("A 收口等待·价站上昨日上轨且量足 → breakout (看多/强/盘中开口向上)")
def _():
r = tr.intraday_confirm(_st("收口等待"), price=10.5, vwap=10.0, upper_prev=10.2,
vol_ratio=2.0, now=NOW, vol_min=1.5)
assert r["kind"] == "breakout" and r["changed"] is True
assert r["state"]["stance"] == "看多" and r["state"]["strength"] == ""
assert r["state"]["phase"] == "盘中开口向上"
@case("A 收口等待·价没过上轨 → 不升 (原样, kind 空)")
def _():
r = tr.intraday_confirm(_st("收口等待"), price=10.1, vwap=10.0, upper_prev=10.2,
vol_ratio=2.0, now=NOW, vol_min=1.5)
assert r["kind"] is None and r["changed"] is False
@case("A 收口等待·量不足 → 不升")
def _():
r = tr.intraday_confirm(_st("收口等待"), price=10.5, vwap=10.0, upper_prev=10.2,
vol_ratio=1.2, now=NOW, vol_min=1.5)
assert r["kind"] is None
@case("A 收口等待·突破开关关掉 → 不升 (breakout_on 假)")
def _():
r = tr.intraday_confirm(_st("收口等待"), price=10.5, vwap=10.0, upper_prev=10.2,
vol_ratio=2.0, now=NOW, vol_min=1.5, breakout_on=False)
assert r["kind"] is None and r["changed"] is False
@case("A 收口等待·昨日上轨取不到 → 不升 (取不到不放行)")
def _():
r = tr.intraday_confirm(_st("收口等待"), price=10.5, vwap=10.0, upper_prev=None,
vol_ratio=2.0, now=NOW, vol_min=1.5)
assert r["kind"] is None
@case("A 开口向上·价≥均价且量足 → opened_ok, 块不变")
def _():
r = tr.intraday_confirm(_st("开口向上", stance="看多"), price=10.5, vwap=10.0,
upper_prev=10.2, vol_ratio=2.0, now=NOW, vol_min=1.5)
assert r["kind"] == "opened_ok" and r["changed"] is False
assert r["state"]["phase"] == "开口向上" and r["state"]["stance"] == "看多"
@case("A 开口向上·价低于均价 → opened_wait")
def _():
r = tr.intraday_confirm(_st("开口向上", stance="看多"), price=9.8, vwap=10.0,
upper_prev=10.2, vol_ratio=2.0, now=NOW, vol_min=1.5)
assert r["kind"] == "opened_wait" and r["changed"] is False
@case("A 开口向上·量不足 → opened_wait")
def _():
r = tr.intraday_confirm(_st("开口向上", stance="看多"), price=10.5, vwap=10.0,
upper_prev=10.2, vol_ratio=1.0, now=NOW, vol_min=1.5)
assert r["kind"] == "opened_wait"
@case("A 开口向上·读数取不到 (现价 None) → opened_wait (取不到按等待)")
def _():
r = tr.intraday_confirm(_st("开口向上", stance="看多"), price=None, vwap=10.0,
upper_prev=10.2, vol_ratio=2.0, now=NOW, vol_min=1.5)
assert r["kind"] == "opened_wait"
@case("A 时段外·开口向上 → outside_window (进观察)")
def _():
early = datetime(2026, 9, 15, 9, 40) # 早于 09:45
r = tr.intraday_confirm(_st("开口向上", stance="看多"), price=10.5, vwap=10.0,
upper_prev=10.2, vol_ratio=2.0, now=early, window="0945-1430")
assert r["kind"] == "outside_window" and r["changed"] is False
@case("A 时段外·收口等待 → 原样 (kind 空, 不动)")
def _():
early = datetime(2026, 9, 15, 9, 40)
r = tr.intraday_confirm(_st("收口等待"), price=10.5, vwap=10.0, upper_prev=10.2,
vol_ratio=2.0, now=early, window="0945-1430")
assert r["kind"] is None and r["changed"] is False
# ================================================================ B 同源哨兵与映射带上轨
@case("B 同源哨兵·tech_rules 的 in_window 就是 reask_rules 那一个函数对象")
def _():
assert tr.in_window is rr.in_window
@case("B 映射带上轨·synthesize base 与 _compact 都带 boll_upper")
def _():
from app.services import tech_service as ts
rows = [{"data_date": 20260914, "reanchored": 1, "quality": "OK", "sar_side": "",
"boll_pos": 0.6, "boll_upper": 12.34, "boll_squeeze": 1, "sar_flip_days": 5,
"bbi_state": "多头区", "sar_value": 11.0, "boll_bw_pct": 3.0}]
st = tr.synthesize(rows)
assert st.get("boll_upper") == 12.34
assert "boll_upper" in ts._compact(st) and ts._compact(st)["boll_upper"] == 12.34
# ================================================================ C 接线 (proposal_service._apply_intraday)
def _cfg(**kw):
c = {"breakout_on": True, "vol_min": 1.5, "window": "0945-1430",
"cparams": {"fund_required": True, "consensus_route": True, "weak_confirm": True}}
c.update(kw)
return c
@case("C 突破·改立场重合议为放行, 硬数字 tech 带 intraday")
def _():
from app.services import proposal_service as pp
blocks = {"fund": {"stance": "看多"}, "timing": {"stance": "中性"},
"tech": {"phase": "收口等待", "boll_upper": 10.2, "stance": "中性"},
"consensus": {"route": "观察", "direction": "中性", "route_reason": "等技术面开口"}}
cache = {"600000.SH": (10.5, 10.0, 2.0)} # 价站上上轨、量足
kind = pp._apply_intraday("600000.SH", blocks, _cfg(), NOW, cache)
assert kind == "breakout"
assert blocks["tech"]["stance"] == "看多" and blocks["tech"]["phase"] == "盘中开口向上"
assert blocks["tech"]["intraday"]["kind"] == "breakout"
assert blocks["consensus"]["direction"] == "看多" and blocks["consensus"]["route"] == "放行"
@case("C 开口未确认·路由改观察带 wait_confirm, 硬数字带 intraday")
def _():
from app.services import proposal_service as pp
from app.core import action_engine as ae
blocks = {"fund": {"stance": "看多"}, "timing": {"stance": "看多"},
"tech": {"phase": "开口向上", "boll_upper": 10.2, "stance": "看多"},
"consensus": {"route": "放行", "direction": "看多"}}
cache = {"600000.SH": (9.5, 10.0, 2.0)} # 价低于均价 → 未确认
kind = pp._apply_intraday("600000.SH", blocks, _cfg(), NOW, cache)
assert kind == "opened_wait"
assert blocks["consensus"]["route"] == "观察"
assert blocks["consensus"]["disp"] == ae.DISP_WAIT_CONFIRM
assert blocks["tech"]["intraday"]["kind"] == "opened_wait"
@case("C 总闸关掉·_intraday_cfg 返回 None (逐字回旧)")
def _():
from app.services import proposal_service as pp
o = pp.param_store.get_bool
pp.param_store.get_bool = lambda k, d=False: (False if k == "PMS_TECH_INTRADAY_CONFIRM"
else o(k, d))
try:
assert pp._intraday_cfg() is None
finally:
pp.param_store.get_bool = o
# ================================================================ E 参数
@case("E 参数·盘中确认四键登记, 默认值对")
def _():
assert ps.RUNTIME_EXTRA["PMS_TECH_INTRADAY_CONFIRM"][0] is True
assert ps.RUNTIME_EXTRA["PMS_TECH_INTRADAY_BREAKOUT"][0] is True
assert abs(ps.RUNTIME_EXTRA["PMS_TECH_VOL_CONFIRM"][0] - 1.5) < 1e-9
assert ps.RUNTIME_EXTRA["PMS_TECH_INTRADAY_WINDOW"][0] == "0945-1430"
@case("E 参数·量比阈值范围校验 0.5 到 10")
def _():
assert ps._range_check("PMS_TECH_VOL_CONFIRM", 0.3) # 低于下限 → 报错串
assert ps._range_check("PMS_TECH_VOL_CONFIRM", 20) # 高于上限 → 报错串
assert ps._range_check("PMS_TECH_VOL_CONFIRM", 1.5) is None
def main():
ok = 0
for name, fn in RESULTS:
try:
fn()
ok += 1
print(" ok " + name)
except Exception:
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())