盘中确认包:给技术面这一票加盘中放量确认,只用于入场(台账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:
parent
94da25bc25
commit
1ccd542898
|
|
@ -51,6 +51,9 @@ BUY, SELL = "buy", "sell"
|
|||
# 候选处置词 (2026-09-11 三源合议工作包二): 合议判「观察」的新建仓候选写这个 disp,
|
||||
# 页面把它渲染成「等技术面开口」。与既有的 would / deny 并列, 走 disposition_snapshot 那条路。
|
||||
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。
|
||||
# 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_BEAR = "open_skip_bear" # 合议判跳过, 其余原因 (方向看空)
|
||||
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)
|
||||
# open_breakout (盘中收口突破视为开口) 不打在跳过项上 —— 突破的候选会被放行、不进 skipped,
|
||||
# 由 proposal_service 记进 consensus_seen 的 intraday 字段, consensus_stats 据此落表。
|
||||
|
||||
# 同一轮里出现多条减持时留哪一条 (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})
|
||||
continue
|
||||
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 "三源合议判为观察,等技术面开口",
|
||||
"tag": TAG_OPEN_WAIT_TECH})
|
||||
"tag": _tag})
|
||||
continue
|
||||
if slots <= 0:
|
||||
skipped.append({"ts_code": code, "action": A_OPEN,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ ddl_pms_v1.sql 的 pms_tech_daily), 最后一行是最新那天。数据新鲜
|
|||
"""
|
||||
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 个交易日」(方案第三节)。窗口本身不设页面参数, 翻向次数阈值才设。
|
||||
CHOPPY_WINDOW = 20
|
||||
|
||||
|
|
@ -101,6 +106,7 @@ def synthesize(rows, *, params=None) -> dict:
|
|||
sar_value = _num(latest.get("sar_value"))
|
||||
|
||||
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}
|
||||
|
||||
# ---- 第二步: 震荡市 ----
|
||||
|
|
@ -156,3 +162,54 @@ def synthesize(rows, *, params=None) -> dict:
|
|||
return out("中性", None, "震荡", f"近 20 日 SAR 翻向 {flips} 次,震荡市不表态")
|
||||
# 兜底: side 已限定在 {多, 空}, 上面各分支理应已覆盖全部组合; 到这里保守中性。
|
||||
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",
|
||||
"开口向上未确认(现价未站上均价或量能不足/取不到),进观察等确认")
|
||||
|
|
|
|||
|
|
@ -42,10 +42,12 @@ STAT_TIMES_KEY = "PMS_CONSENSUS_STAT_TIMES"
|
|||
K_OPEN_SKIP_FUND = ae.TAG_OPEN_SKIP_FUND
|
||||
K_OPEN_SKIP_BEAR = ae.TAG_OPEN_SKIP_BEAR
|
||||
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
|
||||
# 从 consensus_seen / scanned / 映射重建派生的类别 (动作引擎不打这些 tag)。
|
||||
K_OPEN_CONFIRM = "open_confirm"
|
||||
K_OPEN_PASS = "open_pass"
|
||||
K_OPEN_BREAKOUT = "open_breakout" # 盘中确认包: 收口突破视为开口 (从 consensus_seen 的 intraday 派生)
|
||||
K_FUND_NOREAD = "fund_noread"
|
||||
K_TIMING_NOREAD = "timing_noread"
|
||||
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))
|
||||
if s.get("timing") == "无读数":
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -132,6 +132,12 @@ RUNTIME_EXTRA = {
|
|||
# 四位时刻逗号分隔; consensus_stats.record_round 只在这些时刻及其下一分钟写路由与装配两类
|
||||
# (一天最多四次), 转空离场实弹随时写。设为空串即不写检查点行, 只留转空实弹。
|
||||
"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)。**
|
||||
|
|
@ -512,6 +518,7 @@ _RANGES = {
|
|||
"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_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_EXIT_BAND": (0, 100), "PMS_MACRO_CONFIRM_DAYS": (1, 10),
|
||||
|
|
|
|||
|
|
@ -33,10 +33,12 @@ import time
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from app.core import action_engine as ae
|
||||
from app.core import consensus
|
||||
from app.core import copy
|
||||
from app.core import command_spec as cs
|
||||
from app.core import rule_gate
|
||||
from app.core import signal_rules as sr
|
||||
from app.core import tech_rules
|
||||
from app.core import tradedays as td
|
||||
from app.repo import pms_repo
|
||||
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, 不扫描候选池"})
|
||||
else:
|
||||
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:
|
||||
logger.exception("新建仓扫描失败")
|
||||
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) 有意不同的两点:
|
||||
|
|
@ -384,7 +386,7 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
|
|||
# 开关关掉整段不做, cands 原样进 scan_open —— 与接入前逐字相同。装配失败只记日志、不拦扫描
|
||||
# (合议是加不改: 装不上就当没有合议, scan_open 里 con 为 None 自然退回旧路)。
|
||||
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),
|
||||
room_amt=room, slots=slots, skip=skip)
|
||||
out["skipped"].extend(res["skipped"])
|
||||
|
|
@ -403,7 +405,73 @@ def _scan_open(view, params, stock_params, skip, mkt, out) -> list:
|
|||
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_blocks —— 四块意见 (fund / tech / timing / consensus), scan_open 定档时取 fund/tech 喂 advise_v2;
|
||||
|
|
@ -411,8 +479,13 @@ def _attach_consensus(cands, out=None) -> None:
|
|||
批量取一次昨夜定性与技术面映射; 盘中转多留痕从候选自带的 sig_buy 取时刻。任何一步失败都不抛 ——
|
||||
装不上就当这只票没有合议 (scan_open 里 con 为 None 自然退回旧路), 合议是加不改。
|
||||
|
||||
每装配一只候选往 out["consensus_seen"] 追加一条紧凑记录 (观察读数包用, 也给候选处置快照用):
|
||||
ts_code / fund / tech / timing / direction / route / phase。out 为 None 或缺该键时只装配不留痕。"""
|
||||
盘中确认 (2026-09-14 盘中确认包): 装配完四块后, 若总闸开着且传了 now, 对收口等待/开口向上的
|
||||
候选跑一遍 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:
|
||||
return
|
||||
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
|
||||
logger.warning("[合议] 批量取数失败, 本轮候选不装配合议: %s", e)
|
||||
return
|
||||
icfg = _intraday_cfg() if now is not None else None
|
||||
icache: dict = {}
|
||||
for c in cands:
|
||||
code = cs.normalize_code(c.get("ts_code") or "")
|
||||
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
|
||||
blocks = consensus_service.assemble(
|
||||
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"] = blocks
|
||||
c["consensus_hard"] = consensus_service.hard_keys(blocks)
|
||||
if seen is not None:
|
||||
con, fund, tech, tm = (blocks["consensus"], blocks["fund"],
|
||||
blocks["tech"], blocks["timing"])
|
||||
seen.append({"ts_code": code, "fund": fund.get("stance"),
|
||||
"tech": tech.get("stance"), "timing": tm.get("stance"),
|
||||
"direction": con.get("direction"), "route": con.get("route"),
|
||||
"phase": tech.get("phase")})
|
||||
rec = {"ts_code": code, "fund": fund.get("stance"),
|
||||
"tech": tech.get("stance"), "timing": tm.get("stance"),
|
||||
"direction": con.get("direction"), "route": con.get("route"),
|
||||
"phase": tech.get("phase")}
|
||||
if ikind:
|
||||
rec["intraday"] = ikind
|
||||
seen.append(rec)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("[合议] 装配失败 %s (本票按无合议): %s", code, e)
|
||||
|
||||
|
|
@ -612,7 +691,7 @@ def disposition_snapshot(now=None) -> dict:
|
|||
for _k in _inflight_keys(): # 在途; 前端多半已按指令/提议标成「已下单/待你确认」
|
||||
skip[_k] = "系统已经在处理这只票(见『等我拍板』或『今日在办』)"
|
||||
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:
|
||||
logger.exception("候选处置快照失败")
|
||||
return {"ok": False, "ready": False, "error": f"{type(e).__name__}: {e}",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ def _compact(st: dict) -> dict:
|
|||
"phase": st.get("phase"), "confirm": st.get("confirm"),
|
||||
"sar_side": st.get("sar_side"), "sar_value": st.get("sar_value"),
|
||||
"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")}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2882,6 +2882,8 @@ createApp({
|
|||
// 三源合议判观察 (2026-09-11): 等技术面开口, 是「在盯」不是「被否决」——
|
||||
// 归到 watch 那一档而不是今天没建仓那一档, 因为它还会回来。
|
||||
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: 'wait', label: '在盯', why: '' };
|
||||
|
|
|
|||
|
|
@ -93,16 +93,21 @@
|
|||
转空实弹随时写、试算不写、被闸拒不记、confirm 据动作名、空串检查点)/
|
||||
record_map_cover 记覆盖与逐只无读数/日报小节失败不拖垮/upsert 更新子句
|
||||
只用 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 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
|
||||
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
|
||||
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例)
|
||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
||||
目标价到价必定入队 (档位 full 也不自动卖) +
|
||||
用户设的止损价与目标价单独成列显示 (70 例)
|
||||
共 847 例
|
||||
共 866 例
|
||||
(总数按实跑逐批相加校正过两次: 曾写 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;
|
||||
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例、第三十批加三例到 846; 评审修订第三十一批加一例, 现为 847)
|
||||
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例、第三十批加三例到 846; 评审修订第三十一批加一例到 847; 09-14 盘中确认包第三十二批十九例, 现为 866)
|
||||
任一子集失败即整体失败 (退出码 1)。
|
||||
|
||||
哨兵位置清单 (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_batch30_units.py",
|
||||
"test_batch31_units.py",
|
||||
"test_batch32_units.py",
|
||||
"test_page_enum_guard.py",
|
||||
"test_page_wiring_guard.py",
|
||||
"test_wiring.py"]
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
Loading…
Reference in New Issue