akg-factor-bridge/regime.py

126 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""环境标签:只读决策系统的日频市场区制接口,写进当日计划快照;只展示与复盘分组,不作交易前置。
## 为什么要有它
实证docs/主观选股改进方案_2026-09-02.md 1.8、1.8e):市场环境是第一解释变量,但
"看过去五日或十日涨跌"没有预测力(相关 0.28、0.55)。所以桥不自算环境,只读
决策系统已有的、带迟滞的区制判断(主参考指数跌破二十日线且缩量或连续破位判弱势),
把它当标签挂在每张卡上,供复盘按"事前标签"分组。标签有没有预测力由复盘过线条件验证;
过线前不拦任何票,"弱环境不加新票"已退役09-02 拍板)。
## 时序
决策系统 08:40 预热当日快照;桥 07:10 出计划时拿不到,标 UNKNOWN。统一调度平台在 08:45
再触发一次追加步骤xxl.py 的 regime-append把当日快照写进当天 JSON 的 regime 段;
/plan 应答的 regime 段只读那份落盘的快照,盘中不再向来源发请求。
## 接口契约(桥按此消费;决策系统侧按此实现只读端点)
GET {REGIME_API_URL}?date=YYYY-MM-DD ->
{"status": "OK" | "UNKNOWN" | "DISABLED", "data_date": "YYYY-MM-DD",
"indices": [{"code": "000300.SH", "name": "沪深300", "weak": true, "reason": "..."}],
"weak_count": 3, "degraded": false, "computed_at": "..."}
DISABLED 归 UNKNOWN 并在 source 里注明总开关关闭。任何失败 -> UNKNOWN绝不抛错。
"""
from __future__ import annotations
import datetime as dt
import json
import os
import urllib.parse
import urllib.request
import config
UNKNOWN = "UNKNOWN"
def fetch(day: str) -> dict:
"""读一次当日区制。返回归一后的 regime 段。"""
base = {"status": UNKNOWN, "data_date": None, "weak_count": None, "weak_day": None,
"indices": [], "degraded": None, "source": None,
"fetched_at": dt.datetime.now().isoformat(timespec="seconds"),
"weak_threshold": config.REGIME_WEAK_COUNT}
if not config.REGIME_API_URL:
base["source"] = "unconfigured决策系统只读区制接口未接入"
return base
url = f"{config.REGIME_API_URL}?{urllib.parse.urlencode({'date': day})}"
base["source"] = url
try:
with urllib.request.urlopen(url, timeout=config.REGIME_API_TIMEOUT) as resp:
payload = json.loads(resp.read().decode("utf-8", "replace"))
except Exception as e: # noqa: BLE001 —— 来源不可达就是 UNKNOWN不拦票
base["source"] = f"{url}(不可达: {type(e).__name__}"
return base
if not isinstance(payload, dict):
return base
status = str(payload.get("status") or UNKNOWN).upper()
if status == "DISABLED":
base["source"] += "(总开关关闭)"
status = UNKNOWN
# 两种形状都收:契约形状 indices 为列表;决策系统缓存快照的原始形状 indices 为
# {指数代码: {name, weak, weak_via, qrs_stale, ...}}、广度字段叫 breadth_weak。
raw_idx = payload.get("indices")
if isinstance(raw_idx, dict):
indices = [{"code": c, **(v if isinstance(v, dict) else {})} for c, v in raw_idx.items()]
elif isinstance(raw_idx, list):
indices = [x for x in raw_idx if isinstance(x, dict)]
else:
indices = []
weak_count = payload.get("weak_count")
if weak_count is None:
weak_count = payload.get("breadth_weak")
if weak_count is None and indices:
weak_count = sum(1 for x in indices if x.get("weak"))
degraded = payload.get("degraded")
if degraded is None and indices:
degraded = any(x.get("qrs_stale") for x in indices)
base.update({
"status": "OK" if status == "OK" else UNKNOWN,
"data_date": payload.get("data_date"),
"weak_count": weak_count,
"weak_total": payload.get("breadth_total") or (len(indices) or None),
"indices": [{"code": x.get("code"), "name": x.get("name"), "weak": bool(x.get("weak")),
"weak_via": x.get("weak_via"), "qrs_stale": x.get("qrs_stale"),
"dev_pct": x.get("dev_pct")} for x in indices],
"degraded": bool(degraded) if degraded is not None else None,
"computed_at": payload.get("computed_at"),
})
if base["status"] == "OK" and isinstance(weak_count, int):
base["weak_day"] = weak_count >= config.REGIME_WEAK_COUNT
return base
def snapshot_path(day: str) -> str:
return os.path.join(config.PLAN_SNAPSHOT_DIR, f"plan_{day}.json")
def append_to_snapshot(day: str) -> dict:
"""08:45 追加步骤:把当日区制写进当天 JSON 快照的 regime 段。
快照不存在(当日构建失败或 07:10 之前)就只打印,不创建空快照。"""
reg = fetch(day)
path = snapshot_path(day)
if not os.path.exists(path):
print(f"快照 {path} 不存在,环境段未写入(状态 {reg['status']}")
return reg
with open(path, "r", encoding="utf-8") as f:
doc = json.load(f)
doc["regime"] = reg
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(doc, f, ensure_ascii=False, indent=1)
os.replace(tmp, path)
print(f"环境段已写入 {path}status={reg['status']} weak_count={reg['weak_count']} "
f"weak_day={reg['weak_day']}")
return reg
def read_from_snapshot(day: str) -> dict | None:
"""/plan 用:只读当日快照里的 regime 段,没有就 None应答里给 UNKNOWN"""
path = snapshot_path(day)
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f).get("regime")
except (OSError, ValueError):
return None