feat: data-layering 第二批消费层读DB(divergence/staleness/accumulation/collect_eval/strategy_review/mo_provider/multi_timeframe/chip_factors)
This commit is contained in:
@@ -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/月K(period: 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_daily(2026-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:
|
||||
|
||||
Reference in New Issue
Block a user