tradingSystem/app/core/tech_rules.py

216 lines
12 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""技术面相位合成 (2026-09-11 技术面接入, 纯逻辑, 零外部依赖)。
输入是一只票按 data_date 升序的近若干个交易日技术面读数 (tech_repo 落表的行, 字段见
ddl_pms_v1.sql pms_tech_daily), 最后一行是最新那天数据新鲜度 有没有当天行读数
陈不陈旧 tech_service 在调用前保证 (它握着交易日历); 本模块只认传进来的 rows 内容
合成三步, 技术面接入与三源合议方案第三节一一对应:
, 判有没有读数: 没有最新行 / 未除权重锚 / 数据质量非 OK / 关键读数缺失, 一律无读数
无读数是弃权, 绝不折成看空 (设计原则二无读数等于弃权)
, 判是不是震荡市: 最近 20 个交易日 SAR 翻向达到 choppy_flips 次即震荡震荡市里 SAR
翻向不当信号, 这是决策系统文档明说的指标特性
, 判相位: 按固定次序逐条比对, 第一个命中的就是结论 (方案第三节相位表)
立场取值只有四个中文词: 看多 / 看空 / 中性 / 无读数强弱只给看多看空用, 中性与无读数没有
阈值默认值见 DEFAULTS, tech_service 会从参数中心读实际值覆盖; 本模块不读参数中心, 以便脱库单测
"""
from __future__ import annotations
盘中确认包:给技术面这一票加盘中放量确认,只用于入场(台账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>
2026-09-14 11:40:51 +08:00
# 盘中确认包 (2026-09-14): 时段判断复用解锁重问那一份 in_window, 不另写 —— 台账 003
# 「三处同源」的哨兵 (解锁重问、择时、盘中确认共用同一套时段与量比算法)。量比由调用方算,
# 也走 reask_rules.vol_ratio。这里只导入 in_window, 是同一个函数对象。
from app.core.reask_rules import in_window
# 数翻向的窗口「最近 20 个交易日」(方案第三节)。窗口本身不设页面参数, 翻向次数阈值才设。
CHOPPY_WINDOW = 20
DEFAULTS = {
"squeeze_lookback": 5, # 近几个交易日内收过口算「刚收口过」(PMS_TECH_SQUEEZE_LOOKBACK)
"open_bw_growth": 0.20, # 今日带宽比收口日至少扩这么多算开口 (PMS_TECH_OPEN_BW_GROWTH)
"choppy_flips": 4, # 20 日 SAR 翻向达到这么多次算震荡 (PMS_TECH_CHOPPY_FLIPS)
"flip_fresh_days": 2, # SAR 翻向后这么多个交易日内算「刚转向」(PMS_TECH_FLIP_FRESH_DAYS)
}
# 价格「贴下轨」的派生判据 (带内位置 ≤ 此值)。转空确认的次要条件, 主条件是多空布林线空头区。
# 这不是方案里一次定死的五个阈值之一, 是实现派生量; 要调改这里即可。
_NEAR_LOWER = 0.20
_LONG = ""
_SHORT = ""
def _num(v):
try:
return None if v is None else float(v)
except (TypeError, ValueError):
return None
def _truthy(v) -> bool:
"""TINYINT / 布尔 / 字符串都收: 1 / True / "" 为真。"""
if isinstance(v, str):
return v.strip() in ("1", "true", "True", "")
return bool(v)
def _no_read(why: str, latest=None) -> dict:
return {"stance": "无读数", "strength": None, "phase": None, "confirm": None,
"sar_side": None, "sar_value": None, "sar_flip_days": None, "choppy": None,
"reason": why, "no_read_why": why,
"data_date": (latest or {}).get("data_date")}
def count_sar_flips(rows) -> int:
"""rows 升序, 数相邻两行 SAR 方向变化的次数 (只看最近 CHOPPY_WINDOW 行)。"""
seq = [str(r.get("sar_side") or "") for r in (rows or [])[-CHOPPY_WINDOW:]]
seq = [s for s in seq if s in (_LONG, _SHORT)]
return sum(1 for a, b in zip(seq, seq[1:]) if a != b)
def _recent_squeeze_bw(rows, lookback):
"""近 lookback 个交易日内 (不含今日) 最近一次收口那天的带宽; 没收口过返回 None。"""
window = (rows or [])[-(lookback + 1):-1] # 去掉今日那一行
for r in reversed(window): # 由近及远
if _truthy(r.get("boll_squeeze")):
return _num(r.get("boll_bw_pct"))
return None
def synthesize(rows, *, params=None) -> dict:
"""把一只票的近 N 日读数 (升序) 合成技术面立场。返回含 stance/strength/phase/confirm/
sar_value/choppy/reason/data_date/no_read_why 的字典"""
p = dict(DEFAULTS)
if params:
p.update({k: params[k] for k in DEFAULTS if params.get(k) is not None})
rows = list(rows or [])
latest = rows[-1] if rows else None
# ---- 第一步: 有没有读数 (无读数是弃权) ----
if not latest:
return _no_read("没有当天的技术面读数")
if not _truthy(latest.get("reanchored")):
return _no_read("未做除权重锚,读数不可比", latest)
if str(latest.get("quality") or "").upper() != "OK":
return _no_read(f"数据质量非正常({latest.get('quality') or ''}", latest)
side = str(latest.get("sar_side") or "")
pos = _num(latest.get("boll_pos"))
if side not in (_LONG, _SHORT) or pos is None:
return _no_read("关键读数缺失SAR 方向或带内位置)", latest)
squeeze = _truthy(latest.get("boll_squeeze"))
bbi_state = str(latest.get("bbi_state") or "")
bw = _num(latest.get("boll_bw_pct"))
flip_days = latest.get("sar_flip_days")
flip_days = int(flip_days) if flip_days is not None else None
sar_value = _num(latest.get("sar_value"))
base = {"sar_side": side, "sar_value": sar_value, "sar_flip_days": flip_days,
盘中确认包:给技术面这一票加盘中放量确认,只用于入场(台账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>
2026-09-14 11:40:51 +08:00
"boll_upper": _num(latest.get("boll_upper")), # 盘中确认包: 收口突破要拿昨日上轨
"data_date": latest.get("data_date"), "no_read_why": None}
# ---- 第二步: 震荡市 ----
flips = count_sar_flips(rows)
choppy = flips >= int(p["choppy_flips"])
base["choppy"] = choppy
def out(stance, strength, phase, reason, confirm=None):
r = dict(base)
r.update({"stance": stance, "strength": strength, "phase": phase,
"confirm": confirm, "reason": reason})
return r
long_, short_ = side == _LONG, side == _SHORT
fresh_flip = flip_days is not None and flip_days <= int(p["flip_fresh_days"])
bbi_bull = bbi_state == "多头区"
bbi_bear = bbi_state == "空头区"
upper_half = pos >= 0.5
# ---- 第三步: 相位 (固定次序, 第一个命中为结论) ----
# 1 收口等待
if squeeze:
return out("中性", None, "收口等待", "布林带收口,等开口再定方向")
# 2 / 3 开口向上 / 开口向下 (今天已不收口; 近 lookback 日内收过口且带宽扩够)
sq_bw = _recent_squeeze_bw(rows, int(p["squeeze_lookback"]))
opened = (sq_bw is not None and sq_bw > 0 and bw is not None
and (bw / sq_bw - 1) >= p["open_bw_growth"])
if opened and upper_half and long_:
return out("看多", "", "开口向上", "刚收口后放开、价在带内上半部、SAR 多:向上变盘")
if opened and (not upper_half) and short_:
return out("看空", "", "开口向下", "刚收口后放开、价在带内下半部、SAR 空:向下变盘")
# 4 转空 (非震荡, SAR 刚翻空)
if short_ and fresh_flip and not choppy:
confirm = bbi_bear or pos <= _NEAR_LOWER
why = "SAR 刚翻空" + ("、多空布林线空头区或贴下轨(确认)" if confirm else "(待确认)")
return out("看空", "" if confirm else "", "转空", why, confirm=confirm)
# 5 转多 (非震荡, SAR 刚翻多, 多空布林线不在空头区)
if long_ and fresh_flip and not bbi_bear and not choppy:
return out("看多", "", "转多", "SAR 刚翻多、多空布林线未在空头区")
# 6 趋势多 (SAR 多且多空布林线在多头区为强, 中性区为弱)
if long_ and bbi_bull:
return out("看多", "", "趋势多", "SAR 多且多空布林线多头区:上升趋势延续")
if long_ and not bbi_bear:
return out("看多", "", "趋势多", "SAR 多、多空布林线中性区:偏多但不强")
# 7 趋势空 (SAR 空且多空布林线不在多头区)
if short_ and not bbi_bull:
return out("看空", "" if bbi_bear else "", "趋势空", "SAR 空且多空布林线未在多头区:下行")
# 8 分歧 (SAR 与多空布林线方向相反)
if (long_ and bbi_bear) or (short_ and bbi_bull):
return out("中性", None, "分歧", "SAR 与多空布林线方向相反,不表态")
# 9 震荡 (震荡市且以上都不命中)
if choppy:
return out("中性", None, "震荡", f"近 20 日 SAR 翻向 {flips} 次,震荡市不表态")
# 兜底: side 已限定在 {多, 空}, 上面各分支理应已覆盖全部组合; 到这里保守中性。
return out("中性", None, "未归类", "指标组合未归入任何相位,保守中性")
盘中确认包:给技术面这一票加盘中放量确认,只用于入场(台账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>
2026-09-14 11:40:51 +08:00
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",
"开口向上未确认(现价未站上均价或量能不足/取不到),进观察等确认")