109 lines
4.5 KiB
Python
109 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
||
"""market_data.py — 通用行情数据获取库(2026-08-11 从 mr_scanner 抽取)
|
||
|
||
背景:fetch_tx_klines/get_stock_pool 原定义在 mr_scanner.py,
|
||
被 s2_scanner.py import 复用 —— 策略扫描器互相 import 数据函数是坏味道。
|
||
抽取到公共模块,供所有策略扫描器(mr/s2/accumulation/p_oversold)共用。
|
||
"""
|
||
|
||
import json
|
||
import sqlite3
|
||
import urllib.request
|
||
from pathlib import Path
|
||
|
||
from market_config import kline_symbol, market_for_code
|
||
|
||
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
|
||
UA = "Mozilla/5.0"
|
||
|
||
|
||
def fetch_tx_klines(code, datalen=120):
|
||
"""腾讯前复权日K(qfq),与 stock_daily 数据零偏差,返回 [{date,open,close,high,low,volume}]"""
|
||
raw = str(code).strip()
|
||
sym = kline_symbol(raw)
|
||
if sym is None:
|
||
return None
|
||
url = f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?param={sym},day,,,{datalen},qfq"
|
||
try:
|
||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||
with opener.open(req, timeout=8) as r:
|
||
text = r.read().decode("utf-8", errors="replace").strip()
|
||
data = json.loads(text)
|
||
node = data.get("data", {}).get(sym, {})
|
||
bars = node.get("qfqday") or node.get("day") or []
|
||
if not bars or len(bars) < 70:
|
||
return None
|
||
result = []
|
||
for b in bars:
|
||
if len(b) < 6:
|
||
continue
|
||
result.append({
|
||
"date": b[0][:10],
|
||
"open": float(b[1]),
|
||
"close": float(b[2]),
|
||
"high": float(b[3]),
|
||
"low": float(b[4]),
|
||
"volume": float(b[5]), # 手
|
||
})
|
||
return result
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
# 兼容别名(供外部引用)
|
||
fetch_sina_klines = fetch_tx_klines
|
||
|
||
|
||
def get_stock_pool(market='a'):
|
||
"""待扫描股票池:按市场参数化(阶段2B 港股接入)
|
||
|
||
返回 (pool, existing):
|
||
- pool: 待扫描股票池
|
||
- existing: 已有策略/持仓代码(供扫描器跳过)
|
||
|
||
market:
|
||
- 'a'(默认): A股池 = stock_daily distinct 6位 code(与回测 run_mr_backtest 完全同口径,
|
||
含300/688,不含301新创业板——数据源未收录)。实盘扫描用同一口径,保证信号
|
||
覆盖的股票都是回测验证过的。
|
||
- 'hk' : 港股池 = hk_connect_stocks 表 is_active=1 的 code(5位)
|
||
+ 港股持仓/自选(holdings/holding_strategies 里 5 位代码)
|
||
- 'all' : A股池 + 港股池 合并
|
||
|
||
降级:hk_connect_stocks 表可能不存在(阶段2A 并行开发中)→ try/except
|
||
优雅降级,market='hk' 只返回港股持仓/自选,不报错。
|
||
"""
|
||
conn = sqlite3.connect(str(DB_PATH), timeout=5)
|
||
try:
|
||
existing = set()
|
||
for r in conn.execute("SELECT code FROM holding_strategies WHERE status='active'"):
|
||
existing.add(str(r[0]))
|
||
for r in conn.execute("SELECT code FROM holdings WHERE is_active=1"):
|
||
existing.add(str(r[0]))
|
||
# 与回测完全一致:stock_daily 有K线的股票(回测 universe='a' 排除5位港股)
|
||
all_stocks = [str(r[0]) for r in
|
||
conn.execute("SELECT DISTINCT code FROM stock_daily").fetchall()]
|
||
# 港股通名单(阶段2A hk_connect_list 采集器写入;表可能尚不存在 → 优雅降级)
|
||
hk_connect = set()
|
||
try:
|
||
for r in conn.execute("SELECT code FROM hk_connect_stocks WHERE is_active=1"):
|
||
c = str(r[0])
|
||
if len(c) == 5 and c.isdigit():
|
||
hk_connect.add(c)
|
||
except sqlite3.OperationalError as e:
|
||
if "no such table" not in str(e).lower():
|
||
raise
|
||
# 表不存在(阶段2A 并行开发中)→ 仅港股持仓/自选,不报错
|
||
finally:
|
||
conn.close()
|
||
# 只留 A 股(6位数字),排除港股(5位0开头)—— 与回测 is_hk_code 逻辑一致
|
||
a_stocks = [c for c in all_stocks if len(c) == 6 and c.isdigit()]
|
||
# 港股持仓/自选(5位代码,用 market_for_code 判断)
|
||
hk_existing = sorted(c for c in existing if market_for_code(c) == 'hk')
|
||
hk_pool = sorted(set(hk_connect) | set(hk_existing))
|
||
if market == 'hk':
|
||
return hk_pool, set(hk_existing)
|
||
if market == 'all':
|
||
return sorted(set(a_stocks) | set(hk_pool)), existing
|
||
return a_stocks, existing # market='a'(默认):与阶段2B 前完全一致
|