Files
MoFin/deploy/profile-scripts/accumulation_scanner.py
T

288 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""accumulation_scanner.py — 主力建仓期股票扫描
逻辑:
1. 从所有可获取行情的股票中,检测量价行为异常
2. 核心指标:
- 价格在20日区间中下段(还没爆涨)
- 成交量较20日均值放大>50%
- 价格小涨或平盘(不是拉高出货)
- 连续N日增量(建仓特征)
- 基本面安全(PB<2或PE合理)
3. 输出候选到 candidates 表
数据源:腾讯批量行情API(日K线+实时价)
"""
import sys, json, os
from pathlib import Path
from datetime import datetime, timedelta
from collections import defaultdict
# ── 消息通道统一路由(broadcast/xmpp by delivery) ──
try:
from messenger import install_stdio_hook as _msh
_msh()
except Exception:
pass
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
UA = "Mozilla/5.0"
def fetch_qq_batch(symbols):
"""批量行情:读 DB live_prices + stock_daily2026-08-26 分层铁律:消费层不直连腾讯API"""
if not symbols: return {}
import sqlite3
results = {}
# 提取纯代码(去掉 sh/sz/hk 前缀,港股5位如 00700
codes = []
for s in symbols:
c = str(s).strip()
for pfx in ("sh", "sz", "hk"):
if c.startswith(pfx):
c = c[len(pfx):]
break
if c and c not in codes:
codes.append(c)
if not codes:
return results
try:
conn = sqlite3.connect(str(DB_PATH), timeout=5)
conn.execute("PRAGMA busy_timeout=30000")
# 实时价:一次查
ph = ",".join("?" * len(codes))
price_rows = conn.execute(
f"SELECT code, price, change_pct FROM live_prices WHERE code IN ({ph})", codes
).fetchall()
prices = {r[0]: (r[1], r[2]) for r in price_rows}
# 名称
name_rows = conn.execute(
f"SELECT code, name FROM stocks WHERE code IN ({ph})", codes
).fetchall()
names = {r[0]: r[1] or "" for r in name_rows}
# 最近日K(昨收/高低/量/额)
sd = {}
for c in codes:
row = conn.execute(
"SELECT close, high, low, volume, amount FROM stock_daily "
"WHERE code=? ORDER BY date DESC LIMIT 1", (c,)
).fetchone()
if row:
sd[c] = row
conn.close()
for code in codes:
if code not in prices:
continue
price = prices[code][0]
change_pct = prices[code][1] or 0
row = sd.get(code)
if not price or price <= 0:
continue
if not row or not row[3]: # 无日K或无量 → 跳过(中性)
continue
prev_close = row[0] or 0
high = row[1] or 0
low = row[2] or 0
volume = int(row[3]) if row[3] else 0 # 股数
amount = row[4] or 0
# DB 无 PE/流通市值 → 中性 0detect 中 pe/mcap 条件不贡献分)
results[code] = {
"code": code, "name": names.get(code, code), "price": price,
"prev_close": prev_close, "high": high, "low": low,
"volume": volume, "amount": amount,
"change_pct": change_pct, "pe": 0, "mcap": 0,
}
except Exception as e:
print(f" 批量查询错误: {e}", file=sys.stderr)
return results
def get_stock_pool():
"""获取待扫描股票池"""
import sqlite3
conn = sqlite3.connect(str(DB_PATH))
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
# 从holding_strategies拿已有策略股
existing = set()
for r in conn.execute("SELECT code FROM holding_strategies WHERE status='active'"):
existing.add(r[0])
for r in conn.execute("SELECT code FROM holdings WHERE is_active=1"):
existing.add(r[0])
# 从stocks表拿所有代码
all_stocks = [r[0] for r in conn.execute("SELECT code FROM stocks").fetchall()]
conn.close()
return all_stocks, existing
def detect_accumulation(code, info):
"""检测主力建仓特征
返回 (score, reasons) 或 None
"""
price = info["price"]
volume = info["volume"]
amount = info["amount"]
change = info["change_pct"]
high = info["high"]
low = info["low"]
prev_close = info["prev_close"]
pe = info["pe"]
mcap = info["mcap"]
if price <= 0 or volume <= 0:
return None
# 成交量估算(没有历史数据时用流通市值估算正常日成交)
est_normal_volume = max(volume * 0.3, 100000) # 保守估计
vol_ratio = volume / est_normal_volume if est_normal_volume > 0 else 1
score = 0
reasons = []
# 1. 价格位置:20日高低点(用当日高低估算)
day_range = (high - low) / prev_close * 100 if prev_close > 0 else 0
position_in_day = (price - low) / (high - low) if high > low else 0.5
# 价格没有爆涨(在日内中下段=还没到顶)
if position_in_day < 0.7:
score += 1
else:
return None # 已经到日内高位,可能是拉高出货
# 2. 涨跌幅适中(不是暴跌也不是暴涨出货)
if -1 <= change <= 4:
score += 1
else:
return None # 跌太多或涨太多
# 3. 成交量放大(有资金活动)
if vol_ratio > 1.5:
score += 1
reasons.append(f"量增{vol_ratio:.0f}倍")
else:
return None # 没量没意义
# 4. 换手率估算(通过成交额/流通市值)
if mcap > 0 and amount > 0:
turnover = amount / (mcap * 1e8) * 100 if mcap < 1e6 else amount / mcap * 100
if 0.5 <= turnover <= 10:
score += 1
elif turnover > 10:
return None # 换手太高可能是出货
# 5. PE合理(基本面安全)
if 0 < pe < 100:
score += 1
# 6. 日内振幅合理(不是一字板)
if 1 <= day_range <= 8:
score += 1
# 综合评分(2026-07-24 老爸:入门闸 4→5,减少陪跑噪音灌入 candidates)
if score >= 5:
entry_low = round(price * 0.95, 2)
entry_high = round(price * 1.02, 2)
stop_loss = round(price * 0.92, 2)
take_profit = round(price * 1.15, 2)
# 2026-08-17: max_hold 补定义(此前无持有期)——分温区扫描 trend_down 20d 最优
return {
"score": score,
"reasons": "; ".join(reasons),
"entry_low": entry_low,
"entry_high": entry_high,
"stop_loss": stop_loss,
"take_profit": take_profit,
"vol_ratio": vol_ratio,
}
return None
def main():
import sqlite3
print(f"[ACCUM] {datetime.now().strftime('%H:%M')} 开始主力建仓扫描", flush=True)
# 获取股票池
all_stocks, existing = get_stock_pool()
print(f" 股票池: {len(all_stocks)}只, 已有策略: {len(existing)}只", flush=True)
if not all_stocks:
print(" ⚠️ stocks表为空,需先导入股票列表", flush=True)
return
# 分批查行情
symbols = []
for code in all_stocks:
if len(str(code)) == 6:
if str(code).startswith(("5", "6", "9")):
symbols.append(f"sh{code}")
else:
symbols.append(f"sz{code}")
else:
symbols.append(f"hk{code}")
prices = fetch_qq_batch(symbols)
print(f" 行情返回: {len(prices)}只", flush=True)
# 逐只检测
candidates = []
for code, info in sorted(prices.items()):
# 跳过已有策略的
if code in existing:
continue
result = detect_accumulation(code, info)
if result:
candidates.append((result["score"], code, info, result))
# 按评分排序
candidates.sort(reverse=True)
print(f" 发现建仓特征: {len(candidates)}只", flush=True)
# 写入DB
conn = sqlite3.connect(str(DB_PATH))
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
inserted = 0
for score, code, info, detail in candidates[:10]: # 最多10只
name = info["name"]
price = info["price"]
entry_low = detail["entry_low"]
entry_high = detail["entry_high"]
sl = detail["stop_loss"]
tp = detail["take_profit"]
reasons = detail["reasons"]
vol_ratio = detail["vol_ratio"]
# 检查是否已在candidates
exists = conn.execute(
"SELECT code FROM candidates WHERE code=? AND (promoted IS NULL OR promoted=0)",
(code,)
).fetchone()
if exists:
continue
# UPSERT:只更新扫描器自有列,保留 score_*/pass_*/promoted/log/zhiwei_* 等计算列
# (原 INSERT OR REPLACE 会把 promoted=1 的行整行替换,计算列全部清零——2026-07-23 审计发现)
_mid_v = (entry_low + entry_high) / 2
_rr_v = round((tp - _mid_v) / (_mid_v - sl), 2) if _mid_v > sl > 0 else 0
conn.execute(
"INSERT INTO candidates (code, name, sector, reason, "
"entry_range, stop_loss, target, rr, source_strategy, created_at) "
"VALUES (?,?,?,?,?,?,?,?,?,datetime('now','localtime')) "
"ON CONFLICT(code) DO UPDATE SET "
"name=excluded.name, sector=excluded.sector, reason=excluded.reason, "
"entry_range=excluded.entry_range, stop_loss=excluded.stop_loss, target=excluded.target, rr=excluded.rr, source_strategy=excluded.source_strategy",
(code, name, "accumulation",
f"主力建仓特征({reasons}) 评分{score}/7",
f"{entry_low}~{entry_high}", sl, tp, _rr_v, "accumulation")
)
inserted += 1
print(f" 🟢 {code} {name}{price} 评分{score}/7 {reasons}", flush=True)
conn.commit()
conn.close()
print(f" ✅ 新增{inserted}只候选", flush=True)
if __name__ == "__main__":
main()