feat: data-layering 第二批消费层读DB(divergence/staleness/accumulation/collect_eval/strategy_review/mo_provider/multi_timeframe/chip_factors)

This commit is contained in:
xxm
2026-08-26 21:25:32 +08:00
parent bd1ef0c9a4
commit a819033175
8 changed files with 337 additions and 337 deletions
+70 -56
View File
@@ -12,8 +12,6 @@
import json
import os
import urllib.request
import urllib.error
from datetime import datetime, date, timedelta
from typing import Optional
@@ -23,11 +21,7 @@ DATA_DIR = "/home/hmo/web-dashboard/data"
HISTORY_PATH = os.path.join(DATA_DIR, "price_history.json")
# multi_tf_cache.json 已迁移到 DB (mtf_cache 表)
# 腾讯API K线端点
KLINE_URL = "http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={market}{code},{period},,,{count},qfq"
# 腾讯实时行情端点(用于市场前缀判断)
QUOTE_URL = "http://qt.gtimg.cn/q={market}{code}"
# (2026-08-26 分层铁律:K线/行情读 DB stock_daily/live_prices,不再直连腾讯API)
def _write_klines_to_db(code: str, daily: list, weekly: list, monthly: list, fundamentals: dict = None):
@@ -125,8 +119,42 @@ def _save_mtf_cache():
pass
def _aggregate_bars(daily_bars: list, period: str) -> list:
"""把升序日K聚合为周K/月Kperiod: week/month)。
Args:
daily_bars: 升序日K列表 [{date,open,close,high,low,volume}]
period: "week" / "month"
Returns:
升序聚合K线 [{date,open,close,high,low,volume}]
"""
from collections import OrderedDict
groups = OrderedDict()
for b in daily_bars:
try:
dt = datetime.strptime(b["date"], "%Y-%m-%d")
except Exception:
continue
if period == "week":
iso = dt.isocalendar()
key = (iso[0], iso[1]) # (年, ISO周)
else: # month
key = (dt.year, dt.month)
if key not in groups:
groups[key] = dict(b)
else:
g = groups[key]
g["close"] = b["close"]
g["high"] = max(g["high"], b["high"])
g["low"] = min(g["low"], b["low"])
g["volume"] += b["volume"]
g["date"] = b["date"]
return list(groups.values())
def fetch_kline(code: str, period: str = "day", count: int = 120) -> list:
"""腾讯API获取K线数据,优先使用本地缓存
""" DB stock_daily 获取K线数据(2026-08-26 分层铁律:消费层不直连腾讯API)
Args:
code: 股票代码 (如 "300548")
@@ -151,61 +179,47 @@ def fetch_kline(code: str, period: str = "day", count: int = 120) -> list:
if cached_klines and updated_at and (now - updated_at) < _KLINE_CACHE_TTL.get(period, 3600):
return cached_klines
market = _market_prefix(code)
is_index = any(code.startswith(p) for p in ["sh", "sz", "hk"])
# 指数代码已经自带前缀,API直接用code;普通股票需要加market前缀
api_code = code if is_index else f"{market}{code}"
url = f"http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={api_code},{period},,,{count},qfq"
# 读 DB stock_daily2026-08-26 分层铁律:消费层不直连腾讯API)
# 指数代码(sh000001/sz399001)与港股(5位 00700)在 stock_daily 中直接以原 code 存储
raw_code = str(code).split("_")[0]
# 周/月K需要更多日K做聚合:周K≈5日/根,月K≈22日/根,留余量
need = count
if period in ("week", "month"):
need = count * (5 if period == "week" else 22) + 10
try:
req = urllib.request.Request(url, headers=_user_agent())
with urllib.request.urlopen(req, timeout=10) as resp:
raw = json.loads(resp.read().decode("utf-8"))
import sqlite3
db = sqlite3.connect('/home/hmo/MoFin/data/mofin.db', timeout=5)
rows = db.execute(
"SELECT date, open, close, high, low, volume FROM stock_daily "
"WHERE code=? ORDER BY date DESC LIMIT ?", (raw_code, need)
).fetchall()
db.close()
except Exception as e:
return {"error": str(e), "code": code, "period": period}
if not isinstance(raw, dict):
return {"error": f"API returned {type(raw).__name__}", "raw": str(raw)[:200]}
api_data = raw.get("data", {})
if not isinstance(api_data, dict):
return {"error": f"data field is {type(api_data).__name__}", "raw": str(api_data)[:200]}
# 指数代码已经自带前缀(sh000001/sz399001),直接用
# 普通股票代码需要加market前缀(sh600036/sz300750
is_index = any(code.startswith(p) for p in ["sh", "sz", "hk"])
stock_key = code if is_index else f"{market}{code}"
stock_data = api_data.get(stock_key, {})
# 腾讯API的K线字段名: qfqday, qfqweek, qfqmonth
period_key = f"qfq{period}"
klines = stock_data.get(period_key, [])
if not klines:
# 尝试其他字段名
for k in stock_data:
if isinstance(stock_data[k], list) and len(stock_data[k]) > 0:
if isinstance(stock_data[k][0], list) and len(stock_data[k][0]) >= 6:
klines = stock_data[k]
break
result = []
for k in klines:
if len(k) >= 6:
try:
result.append({
"date": str(k[0]),
"open": float(k[1]),
"close": float(k[2]),
"high": float(k[3]),
"low": float(k[4]),
"volume": float(k[5]),
})
except (ValueError, IndexError):
bars = []
for r in reversed(rows): # 升序(旧→新),与腾讯API输出时序一致
try:
if r[1] is None or r[2] is None:
continue
bars.append({
"date": str(r[0]),
"open": float(r[1]),
"close": float(r[2]),
"high": float(r[3] or r[2]),
"low": float(r[4] or r[2]),
"volume": float(r[5] or 0),
})
except (ValueError, TypeError):
continue
return result
if period == "day":
return bars[-count:]
# 周/月K:由日K聚合(open=首日open, close=末日close, high=max, low=min, volume=sum
return _aggregate_bars(bars, period)[-count:]
def calc_moving_averages(klines: list, windows: list = [5, 10, 20, 60]) -> dict: