akg-factor-bridge/regime.py

132 lines
5.9 KiB
Python
Raw Normal View History

"""环境标签:只读决策系统的日频市场区制接口,写进当日计划快照;只展示与复盘分组,不作交易前置。
## 为什么要有它
实证docs/主观选股改进方案_2026-09-02.md 1.81.8e市场环境是第一解释变量
"看过去五日或十日涨跌"没有预测力相关 0.280.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_section(day: str, key: str):
"""只读当日快照里的某一段regime、market 等顶层键),快照不存在或坏 JSON 返回 None。
/plan 应答里的环境类字段一律从落盘快照取盘中不向任何来源发请求"""
path = snapshot_path(day)
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f).get(key)
except (OSError, ValueError):
return None
def read_from_snapshot(day: str) -> dict | None:
"""/plan 用:只读当日快照里的 regime 段,没有就 None应答里给 UNKNOWN"""
return read_section(day, "regime")