From bd744c252c30fd5c9e4d341119e2eb9bdc0f3f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Thu, 9 Jul 2026 19:42:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8E=86=E5=8F=B2K=E7=BA=BF(Sina)?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8S2=E5=A4=9A=E6=97=A5=E7=A1=AE=E8=AE=A4+10?= =?UTF-8?q?=E5=8F=AA=E6=96=B0=E5=80=99=E9=80=89=E5=85=A5=E8=87=AA=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/candidate_filter.py | 119 ++++++++++++++++++---------------- scripts/import_full_stocks.py | 107 ++++++++++++++++++++++++++++++ scripts/price_monitor.py | 7 ++ scripts/promote_candidates.py | 41 +++++++----- 4 files changed, 200 insertions(+), 74 deletions(-) create mode 100644 scripts/import_full_stocks.py diff --git a/scripts/candidate_filter.py b/scripts/candidate_filter.py index e1b8cd56..e33ab5b3 100644 --- a/scripts/candidate_filter.py +++ b/scripts/candidate_filter.py @@ -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 diff --git a/scripts/import_full_stocks.py b/scripts/import_full_stocks.py new file mode 100644 index 00000000..ef6c3e72 --- /dev/null +++ b/scripts/import_full_stocks.py @@ -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() diff --git a/scripts/price_monitor.py b/scripts/price_monitor.py index d9038445..093dfdac 100644 --- a/scripts/price_monitor.py +++ b/scripts/price_monitor.py @@ -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() diff --git a/scripts/promote_candidates.py b/scripts/promote_candidates.py index d8ab8cc8..be212ea3 100644 --- a/scripts/promote_candidates.py +++ b/scripts/promote_candidates.py @@ -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