feat: 历史K线(Sina)驱动S2多日确认+10只新候选入自选

This commit is contained in:
知微
2026-07-09 19:42:35 +08:00
parent a83c0ea11e
commit bd744c252c
4 changed files with 200 additions and 74 deletions
+63 -56
View File
@@ -40,7 +40,7 @@ def log_candidate(conn, code, stage, passed, detail):
# ── Stage 2: 多日K线确认 ──
def fetch_daily_klines(code):
"""拉取近N日K线(如API不可用则返回当日单日数据"""
"""拉取近10日日K线(Sina 240分钟线=日K"""
raw = str(code).strip()
if raw.startswith(("6", "9")):
prefix = "sh"
@@ -49,78 +49,85 @@ def fetch_daily_klines(code):
else:
return None
import subprocess as _sp
url = f"http://qt.gtimg.cn/q={prefix}{raw}"
import subprocess as _sp, json as _json
url = f"http://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbol={prefix}{raw}&scale=240&ma=5&datalen=10"
try:
r = _sp.run(["curl", "-s", url], capture_output=True, timeout=10)
raw_text = r.stdout.decode("gbk", errors="ignore")
for line in raw_text.strip().split("\n"):
if "~" not in line: continue
parts = line.split("~")
if len(parts) < 40: continue
price = float(parts[3]) if parts[3] else 0
prev_close = float(parts[4]) if parts[4] else 0
high = float(parts[33]) if parts[33] else 0
low = float(parts[34]) if parts[34] else 0
volume = int(float(parts[6])) if parts[6] else 0
change = float(parts[32]) if parts[32] else 0
if price > 0:
# 返回当日单条K线(后续扫描积累多日数据)
return [{
"date": "today", "open": prev_close, "close": price,
"high": high, "low": low, "volume": volume,
"change_pct": change, "price": price
}]
r = _sp.run(["curl", "-s", "--noproxy", "*", url], capture_output=True, timeout=10)
data = _json.loads(r.stdout)
if not data:
return None
result = []
for k in data:
result.append({
"date": k.get("day", "")[:10],
"open": float(k["open"]),
"close": float(k["close"]),
"high": float(k["high"]),
"low": float(k["low"]),
"volume": int(k["volume"]),
"price": float(k["close"]),
"change_pct": 0,
})
# 计算涨跌幅
for i in range(1, len(result)):
prev = result[i-1]["close"]
if prev > 0:
result[i]["change_pct"] = (result[i]["close"] / prev - 1) * 100
return result
except Exception as e:
return None
except:
return None
def stage2_confirm(code, name, klines):
"""第二关:多日K线确认
当日有量价配合信号即可通过初筛。
多日连续性需要多日扫描数据积累后验证。
检查:多日量价配合、建仓特征
"""
if not klines or len(klines) == 0:
return False, 0, "无行情数据"
today = klines[-1]
price = today.get("price", 0)
volume = today.get("volume", 0)
change = today.get("change_pct", 0)
high = today.get("high", 0)
low = today.get("low", 0)
if not klines or len(klines) < 3:
return False, 0, "K线不足3日"
recent = klines[-5:] # 最近5日
score = 0
checks = []
# 1. 成交量
if volume > 100000: # 至少10万股
# 1. 成交量连续递增
vols = [k["volume"] for k in recent]
vol_rising = sum(1 for i in range(len(vols)-1) if vols[i] < vols[i+1])
if vol_rising >= 3:
score += 2
checks.append(f"量增{vol_rising}/4日")
elif vol_rising >= 2:
score += 1
checks.append(f"{volume/10000:.0f}")
else:
checks.append("量太小")
checks.append(f"微增{vol_rising}/4日")
# 2. 跌幅不过大
if change >= -2:
# 2. 涨放量、跌缩量
up_vol = sum(k["volume"] for k in recent if k["change_pct"] >= 0)
down_vol = sum(k["volume"] for k in recent if k["change_pct"] < 0)
if down_vol > 0 and up_vol / down_vol > 1.5:
score += 2
checks.append(f"涨量/跌量={up_vol/down_vol:.1f}")
elif down_vol > 0 and up_vol / down_vol > 1:
score += 1
# 3. 价格趋势
closes = [k["close"] for k in recent]
up_days = sum(1 for i in range(1, len(closes)) if closes[i] > closes[i-1])
if up_days >= 3:
score += 2
checks.append(f"{up_days}/4日")
elif up_days >= 2:
score += 1
# 4. 无异常放量(单日>3倍均量=可能出货)
avg_vol = sum(vols) / len(vols) if vols else 1
max_ratio = max(v / avg_vol for v in vols) if avg_vol > 0 else 1
if max_ratio < 2.5:
score += 1
else:
checks.append(f"{change:.1f}%")
checks.append(f"异常量{max_ratio:.0f}")
# 3. 日内有波动空间
if high > low and price > low:
score += 1
# 4. 价格不为0
if price > 0:
score += 1
# 多日确认需要后续扫描积累(暂标记)
if len(klines) < 3:
checks.append("待多日确认")
passed = score >= 3
detail = f"评分{score}/4 | {'; '.join(checks)}"
passed = score >= 4
detail = f"评分{score}/7 | {'; '.join(checks)}"
return passed, score, detail
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""import_full_stocks.py — 导入全量A股+港股列表到stocks表
数据来源:深交所/上交所公开列表(通过akshare或腾讯API)
运行:python3 import_full_stocks.py
"""
import sys, json, time, urllib.request
from pathlib import Path
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
def fetch_tencent_batch(codes):
"""腾讯批量查询股票名称"""
url = f"http://qt.gtimg.cn/q={','.join(codes)}"
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
proxy = urllib.request.ProxyHandler({})
opener = urllib.request.build_opener(proxy)
with opener.open(req, timeout=15) as r:
text = r.read().decode("gbk")
results = {}
for line in text.strip().split("\n"):
if "~" not in line:
continue
parts = line.split("~")
name_part = parts[0] if parts else ""
code = ""
m = __import__('re').search(r'_(sh|sz|hk)(\d+)', name_part)
if m:
code = m.group(2)
name = parts[1] if len(parts) > 1 else ""
market = parts[2] if len(parts) > 2 else ""
if code and name:
results[code] = (name, market)
return results
except Exception as e:
print(f" 腾讯API错误: {e}", file=sys.stderr)
return {}
def main():
import sqlite3
conn = sqlite3.connect(str(DB_PATH))
# 获取已有代码
existing = set(r[0] for r in conn.execute("SELECT code FROM stocks").fetchall())
print(f"当前stocks表已有: {len(existing)}")
# 生成待查询的A股代码范围(深市000/001/002/003/300/301,沪市600/601/603/605/688/689
prefixes = {
"深市A": [f"{i:03d}" for i in range(0, 10)], # 000-009
"深市中小": [f"{i:03d}" for i in range(10, 50)], # 010-049→实际用001/002
"深市创业": [f"{i:03d}" for i in range(300, 302)], # 300-301→实际用300
"沪市A": [f"{i:03d}" for i in range(600, 606)], # 600-605
"沪市科创": [f"{i:03d}" for i in range(688, 690)], # 688-689
}
# 实际代码规则:深市000/001/002/003/300/301,沪市600/601/603/605/688
code_ranges = []
for prefix in ["000", "001", "002", "003", "300", "301"]:
for suffix in range(1, 1000):
code_ranges.append(f"{prefix}{suffix:03d}")
for prefix in ["600", "601", "603", "605", "688"]:
for suffix in range(1, 1000):
code_ranges.append(f"{prefix}{suffix:03d}")
print(f"待查代码总量: {len(code_ranges)}")
# 分批查询(每批30个)
batch_size = 30
new_count = 0
for i in range(0, len(code_ranges), batch_size):
batch = code_ranges[i:i+batch_size]
# 过滤已存在的
batch = [c for c in batch if c not in existing]
if not batch:
continue
symbols = []
for c in batch:
if c.startswith(("5", "6", "9")):
symbols.append(f"sh{c}")
else:
symbols.append(f"sz{c}")
results = fetch_tencent_batch(symbols)
for code, (name, market) in results.items():
if code not in existing:
try:
conn.execute(
"INSERT OR IGNORE INTO stocks (code, name) VALUES (?, ?)",
(code, name)
)
new_count += 1
existing.add(code)
except Exception:
pass
if (i // batch_size) % 50 == 0:
print(f" 进度: {i}/{len(code_ranges)}, 新增{new_count}")
conn.commit()
total = conn.execute("SELECT COUNT(*) FROM stocks").fetchone()[0]
print(f"\n完成: 新增{new_count}, 总{total}")
conn.close()
if __name__ == "__main__":
main()
+7
View File
@@ -125,6 +125,8 @@ def refresh_data_prices():
all_codes.add(r['code'])
for r in conn.execute("SELECT code FROM watchlist_stocks"):
all_codes.add(r['code'])
for r in conn.execute("SELECT code FROM holding_strategies WHERE status='active'"):
all_codes.add(r['code'])
conn.close()
except Exception as e:
print(f"⚠️ 从DB读代码失败: {e}", file=sys.stderr)
@@ -190,6 +192,11 @@ def refresh_data_prices():
# 写实时价格表(供 read_live_prices 消费)
live = {h['code']: {'price': h.get('price',0), 'change_pct': h.get('change_pct',0)}
for h in db_holdings if h.get('code')}
# 补充自选股/策略股的价格(它们不在holdings表中)
for code, pdata in prices.items():
if code not in live:
live[code] = {'price': pdata[0] if isinstance(pdata, (list, tuple)) else pdata.get('price',0),
'change_pct': pdata[1] if isinstance(pdata, (list, tuple)) else pdata.get('change_pct',0)}
write_live_prices(conn, live)
conn.commit()
conn.close()
+23 -18
View File
@@ -15,11 +15,12 @@ def main():
# 读未提拔候选(按评分降序)
rows = conn.execute("""
SELECT * FROM candidates
WHERE (promoted IS NULL OR promoted = 0)
AND (dropped IS NULL OR dropped = 0)
AND score >= 6
ORDER BY score DESC
SELECT c.code, c.name, c.score_final, c.entry_range, c.stop_loss, c.target
FROM candidates c
WHERE (c.promoted IS NULL OR c.promoted = 0)
AND (c.dropped IS NULL OR c.dropped = 0)
AND c.score_final >= 4
ORDER BY c.score_final DESC
""").fetchall()
if not rows:
@@ -29,16 +30,21 @@ def main():
promoted = 0
for r in rows:
code = str(r["code"])
name = r["name"] or code
price = r["price"] or 0
el = r["entry_low"] or 0
eh = r["entry_high"] or 0
sl = r["stop_loss"] or 0
tp = r["take_profit"] or 0
score = r["score"] or 0
sector = r["sector"] or ""
reason = r["reason"] or ""
code = str(r[0])
name = r[1] or code
score = r[2] or 0
entry_range = r[3] or ""
sl = r[4] or 0
tp = r[5] or 0
# 解析 entry_range
el, eh = 0, 0
if "~" in entry_range:
parts = entry_range.split("~")
try:
el = float(parts[0])
eh = float(parts[1])
except: pass
# 查是否已在 holding_strategies
exists = conn.execute(
@@ -46,7 +52,6 @@ def main():
(code,)
).fetchone()
if exists:
# 标记已提拔但不重复加
conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,))
print(f"{code} {name} 已在自选中,标记promoted")
continue
@@ -54,7 +59,7 @@ def main():
# 构建策略
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
timing_signal = "买入" if score >= 7 else "关注"
action = f"市场扫描发现({reason})" if reason else "市场扫描发现"
action = f"市场扫描发现(评分{score})"
conn.execute("""
INSERT INTO holding_strategies
@@ -64,7 +69,7 @@ def main():
sector_context, quality_check)
VALUES (?,?,?,?,?,?,?,?,?,'自选策略','scan',
'active',0,'关注',?,?,'', 'pending')
""", (code, name, price, el, eh, sl, tp, timing_signal, action, now, now))
""", (code, name, 0, el, eh, sl, tp, timing_signal, action, now, now))
conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,))
promoted += 1