205 lines
9.5 KiB
Python
205 lines
9.5 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""解锁重问的取数与落账 (2026-09-09 方案第四之一节, 台账 055)。薄壳: 判定全在 core/reask_rules。
|
|||
|
|
|
|||
|
|
两个出口, 职责分得很开:
|
|||
|
|
evaluate_all() 只读、零副作用。扫描每分钟一跳都会调它, 也给持仓页的处置说明用。
|
|||
|
|
**它绝不写任何东西** —— 这是它能被每分钟调用的前提。
|
|||
|
|
commit() 真的要发出研判请求的那一刻才调, 往评审账本写一行。写成功才算用掉
|
|||
|
|
这只票当天那唯一一次重问名额。
|
|||
|
|
|
|||
|
|
为什么名额在"真发请求"时才算用掉, 而不是在解锁时: 解锁只是"允许被重新评估"。解锁之后
|
|||
|
|
候选可能因为名额不够、金额不够而根本没产出, 也可能被规则闸拦下 —— 那些情况下什么都没
|
|||
|
|
问过, 凭什么消耗机会。试算 (dry_run) 同理, 一律不落账、不带强制标记。
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
from app.core import reask_rules as rr
|
|||
|
|
from app.core import signal_rules as sr
|
|||
|
|
from app.repo import downstream_repo, pms_repo
|
|||
|
|
from app.services import market, param_store
|
|||
|
|
|
|||
|
|
logger = logging.getLogger("pms.reask")
|
|||
|
|
|
|||
|
|
REASK_ACTION = "OPEN_REASK"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def params() -> dict:
|
|||
|
|
"""七个阈值 + 总开关。全部登记在运行参数表里, 页面上能改、即时生效。"""
|
|||
|
|
return {
|
|||
|
|
"enabled": param_store.get_bool("PMS_OPEN_REASK_ENABLED", True),
|
|||
|
|
"min_gap_min": param_store.get_int("PMS_OPEN_REASK_MIN_GAP_MIN", 20),
|
|||
|
|
"max_per_day": param_store.get_int("PMS_OPEN_REASK_MAX_PER_DAY", 1),
|
|||
|
|
"window": str(param_store.get("PMS_OPEN_REASK_WINDOW", "0945-1430") or "0945-1430"),
|
|||
|
|
"ref_drift": param_store.get_float("PMS_OPEN_REASK_REF_DRIFT", 0.03),
|
|||
|
|
"up_min": param_store.get_float("PMS_OPEN_REASK_UP", 0.02),
|
|||
|
|
"vol_min": param_store.get_float("PMS_OPEN_REASK_VOL_MIN", 1.5),
|
|||
|
|
"vol_mult": param_store.get_float("PMS_OPEN_REASK_VOL_MULT", 1.3),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def base_snapshot(ts_code: str, price: float, judge_conf=None) -> dict:
|
|||
|
|
"""驳回那一刻的基线快照, 落进驳回行的硬数字里 (键 reask_base)。
|
|||
|
|
|
|||
|
|
取数任何一项失败写 None, **不阻断驳回落账** —— 记账优先于留痕。
|
|||
|
|
旧的驳回行没有这个块, 那时 U3 (价升且量能确认) 就自动不成立, U1、U2 照常可判。
|
|||
|
|
|
|||
|
|
两种口径的当日涨幅都记 (台账 055 第二条): 规则闸的不追高量的是**相对今日开盘**,
|
|||
|
|
解锁条件量的是**相对昨收**。两个数并存, 复盘时才对得上账。
|
|||
|
|
"""
|
|||
|
|
out = {"at": datetime.now().isoformat(timespec="seconds"), "price": float(price or 0),
|
|||
|
|
"chg_prev": None, "chg_open": None, "vol_ratio": None,
|
|||
|
|
"judge_conf": judge_conf, "y_signal": None, "support": None, "pressure": None}
|
|||
|
|
try:
|
|||
|
|
pc = market.get_last_close(ts_code)
|
|||
|
|
if pc and price:
|
|||
|
|
out["chg_prev"] = round(float(price) / float(pc) - 1.0, 6)
|
|||
|
|
day = market.day_snapshot(ts_code) or {}
|
|||
|
|
if day.get("day_chg_from_open") is not None:
|
|||
|
|
out["chg_open"] = round(float(day["day_chg_from_open"]), 6)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 基线涨幅取不到 [%s]: %s", ts_code, e)
|
|||
|
|
try:
|
|||
|
|
out["vol_ratio"] = _vol_ratio(ts_code)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 基线量比取不到 [%s]: %s", ts_code, e)
|
|||
|
|
try:
|
|||
|
|
refs = downstream_repo.fetch_refs(ts_code) or {}
|
|||
|
|
out["y_signal"] = refs.get("signal_type")
|
|||
|
|
out["support"] = _f(refs.get("support"))
|
|||
|
|
out["pressure"] = _f(refs.get("pressure"))
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 基线底牌取不到 [%s]: %s", ts_code, e)
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _f(v):
|
|||
|
|
try:
|
|||
|
|
return float(v)
|
|||
|
|
except (TypeError, ValueError):
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _vol_ratio(ts_code: str):
|
|||
|
|
dv = market.day_volume(ts_code) or {}
|
|||
|
|
return rr.vol_ratio(dv.get("vol"), market.get_vol_base5(ts_code), dv.get("frac"))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _now_read(ts_code: str, buy_at: str) -> dict:
|
|||
|
|
"""此刻的读数。每一项各自 try —— 一路取不到只让相关的那条解锁条件不成立, 不牵连其余。"""
|
|||
|
|
out = {"price": None, "vol_ratio": None, "y_signal": None, "support": None,
|
|||
|
|
"pressure": None, "bionic_buy_at": buy_at or ""}
|
|||
|
|
try:
|
|||
|
|
out["price"] = market.get_price(ts_code)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 现价取不到 [%s]: %s", ts_code, e)
|
|||
|
|
try:
|
|||
|
|
out["vol_ratio"] = _vol_ratio(ts_code)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 量比取不到 [%s]: %s", ts_code, e)
|
|||
|
|
try:
|
|||
|
|
refs = downstream_repo.fetch_refs(ts_code) or {}
|
|||
|
|
out["y_signal"] = refs.get("signal_type")
|
|||
|
|
out["support"] = _f(refs.get("support"))
|
|||
|
|
out["pressure"] = _f(refs.get("pressure"))
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 底牌取不到 [%s]: %s", ts_code, e)
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def evaluate_all(judge_keys: dict, now=None) -> dict:
|
|||
|
|
"""对当日被研判驳回的新建仓逐只判解锁。**只读, 零副作用。**
|
|||
|
|
|
|||
|
|
入参是 proposal_service._judge_rejected_open_keys() 的产出 {(代码, 动作): 原因}。
|
|||
|
|
返回 {(代码, 动作): {"why", "hits", "base", "now"}} —— 只含判为解锁的那些。
|
|||
|
|
解锁条件一条都不成立时返回空字典, 于是当日闸行为与今天逐字相同。
|
|||
|
|
"""
|
|||
|
|
now = now or datetime.now()
|
|||
|
|
p = params()
|
|||
|
|
if not p["enabled"] or not judge_keys:
|
|||
|
|
return {}
|
|||
|
|
codes = [c for (c, a) in judge_keys]
|
|||
|
|
if not codes:
|
|||
|
|
return {}
|
|||
|
|
since = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|||
|
|
try:
|
|||
|
|
rejects = pms_repo.judge_rejected_open_rows(since)
|
|||
|
|
used = pms_repo.reask_count_today(since)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 读当日驳回行失败 (本轮不解锁任何票): %s", e)
|
|||
|
|
return {}
|
|||
|
|
try:
|
|||
|
|
buys = pms_repo.buy_signals_today(since) or {}
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 读当日转多留痕失败 (U1 本轮不成立): %s", e)
|
|||
|
|
buys = {}
|
|||
|
|
|
|||
|
|
out = {}
|
|||
|
|
for key in judge_keys:
|
|||
|
|
code, action = key
|
|||
|
|
row = rejects.get(code) or {}
|
|||
|
|
base = dict((row.get("hard") or {}).get("reask_base") or {})
|
|||
|
|
if not base.get("at") and row.get("at"):
|
|||
|
|
# 旧驳回行没有基线块: 至少把时刻与价格补上, 让 G1 与 U3 的价格那半段能判
|
|||
|
|
base.setdefault("at", str(row["at"]).replace(" ", "T"))
|
|||
|
|
base.setdefault("price", row.get("price"))
|
|||
|
|
b = buys.get(code) or {}
|
|||
|
|
buy_at = b.get("at") or ""
|
|||
|
|
if buy_at and not sr.is_bionic_buy_note(b.get("reason")):
|
|||
|
|
buy_at = "" # 不是决策系统发的转多留痕, 不算 U1
|
|||
|
|
try:
|
|||
|
|
r = rr.evaluate(base=base, now_read=_now_read(code, str(buy_at).replace(" ", "T")),
|
|||
|
|
params=p, used_today=int(used.get(code) or 0), now=now)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 判定出错 [%s] (按不解锁): %s", code, e)
|
|||
|
|
continue
|
|||
|
|
if r.get("unlock"):
|
|||
|
|
out[key] = {"why": r.get("why"), "hits": r.get("hits") or [],
|
|||
|
|
"base": base, "now": _now_read(code, buy_at), "notes": r.get("notes") or []}
|
|||
|
|
logger.info("[重问] 解锁 %s: %s (命中 %s)", code, r.get("why"), ",".join(r.get("hits") or []))
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def commit(ts_code: str, detail: dict, price) -> bool:
|
|||
|
|
"""把这一次重问记进评审账本。写成功才算用掉当天那唯一一次名额。
|
|||
|
|
|
|||
|
|
verdict 用 NOTE 不用 PASS: 这一行不是一个判决, 是"当日闸在这里被解除了"这件事的留痕。
|
|||
|
|
"""
|
|||
|
|
hits = ",".join((detail or {}).get("hits") or [])
|
|||
|
|
reason = "研判驳回闸当日解除,重问一次(命中 %s):%s" % (hits or "无", (detail or {}).get("why") or "")
|
|||
|
|
try:
|
|||
|
|
pms_repo.insert_ledger(ts_code=ts_code, action=REASK_ACTION, arbiter="rule",
|
|||
|
|
verdict="NOTE", price_at=price,
|
|||
|
|
hard_numbers={"reask": {"hits": (detail or {}).get("hits") or [],
|
|||
|
|
"base": (detail or {}).get("base") or {},
|
|||
|
|
"now": (detail or {}).get("now") or {}}},
|
|||
|
|
reason=reason[:500])
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning("[重问] 名额落账失败 [%s] (本轮不重问): %s", ts_code, e)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def reask_text(detail: dict, seq: int = 1) -> str:
|
|||
|
|
"""送研判时随请求带的一句整句, 让模型看得见"输入变了什么"。
|
|||
|
|
|
|||
|
|
与 09-02 把候选卡判决整句送进去是同一手法: 送人话, 不送原值字典。
|
|||
|
|
"""
|
|||
|
|
d = detail or {}
|
|||
|
|
base, now = d.get("base") or {}, d.get("now") or {}
|
|||
|
|
bits = []
|
|||
|
|
if base.get("at"):
|
|||
|
|
bits.append("%s 曾驳回于 %s" % (str(base["at"])[11:16], base.get("price")))
|
|||
|
|
if now.get("price") is not None and base.get("price"):
|
|||
|
|
try:
|
|||
|
|
bits.append("现价 %.2f(%+.2f%%)" % (float(now["price"]),
|
|||
|
|
(float(now["price"]) / float(base["price"]) - 1) * 100))
|
|||
|
|
except (TypeError, ValueError, ZeroDivisionError):
|
|||
|
|
pass
|
|||
|
|
if base.get("vol_ratio") is not None and now.get("vol_ratio") is not None:
|
|||
|
|
bits.append("时段折算量比 %s→%s" % (base["vol_ratio"], now["vol_ratio"]))
|
|||
|
|
return ("本次为当日第 %d 次重问:%s。解除当日闸的依据:%s"
|
|||
|
|
% (seq, ";".join(bits) or "输入已变化", d.get("why") or ""))[:600]
|