From b32e2fd803374bea5fb705bad9d2c93bc2180544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Mon, 22 Jun 2026 20:00:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=A1=8C=E4=B8=9A?= =?UTF-8?q?=E9=A2=86=E6=B6=A8=E8=82=A1=E6=89=AB=E6=8F=8F=20xiaoguo=5Fscann?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 fetch_sector_leaders() 从 market.json 读取热门行业领涨股 - 三路并行:同花顺技术榜 + 行业领涨 + 东财热榜(502降级) - 优先级排序:行业领涨 > 同花顺榜 > 东财热榜 - 名称→代码映射使用本地缓存,避免频繁调用akshare - 更新文档 --- data/mofin.db-shm | Bin 0 -> 32768 bytes data/mofin.db-wal | Bin 0 -> 41232 bytes docs/xiaoguo-signal-pipeline.md | 12 +++++-- xiaoguo_scanner.py | 59 ++++++++++++++++++++++++++++++-- 4 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 data/mofin.db-shm create mode 100644 data/mofin.db-wal diff --git a/data/mofin.db-shm b/data/mofin.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..d7bc782e364b5e6cb31e974728a203545f05e5bd GIT binary patch literal 32768 zcmeI*zik3B6bInP|3H;0qCkQgi5?`TVE~F`fTYSMFvE$08CW5qMiCQ$O?3qY*G1g- zr026N`{duZfQP@c$kj~Uh31G1?q{Cc}#R^S^U- zQeR4STPzK&G+(FTGxb;Me205$r6Z;U2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009Cu5okuQCP~F4 zK!89U1RBw;LrfV75FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7e?KNaXiex*ne rAV7cs0RjXF5FkK+KsA9@yi_}2H-V=SXh;8PvIs(e009C79u>F(_x&tw literal 0 HcmV?d00001 diff --git a/data/mofin.db-wal b/data/mofin.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..d666b340360154ead50540d1ae38b7d47c389c90 GIT binary patch literal 41232 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 LaX(}%(jELCW2.5%板块的领涨龙头 +3. **东方财富热榜**(akshare.stock_hot_rank_em)— 因502不可用,降级静默 + +### 三路数据合并规则 +优先级:行业领涨 > 同花顺技术榜 > 东方财富热榜 +同只股票不重复处理,最多15只/轮。 +行业领涨股保证能被扫描到,不会被技术榜单的股票挤掉。 ### 新闻来源 - 东方财富个股新闻API(akshare.stock_news_em) diff --git a/xiaoguo_scanner.py b/xiaoguo_scanner.py index 53b93f3..bc158bb 100644 --- a/xiaoguo_scanner.py +++ b/xiaoguo_scanner.py @@ -45,6 +45,10 @@ BEARISH_BOARDS = [ ALL_BOARDS = BULLISH_BOARDS + BEARISH_BOARDS BULLISH_COUNT = len(BULLISH_BOARDS) +# 行业领涨股扫描(不轮换,每轮都跑) +# 从 market.json 读热门行业领涨股 +SECTOR_HOT_THRESHOLD = 2.5 # 板块涨幅>2.5%时捞它的领涨股 + def clean_proxy(): for k in ['http_proxy','https_proxy','HTTP_PROXY','HTTPS_PROXY']: @@ -112,6 +116,53 @@ def fetch_rotating_board(): return [], is_bullish +def fetch_sector_leaders(): + """从 market.json 读热门行业领涨股""" + mkt_path = DATA_DIR / "market.json" + if not mkt_path.exists(): + return [] + try: + mkt = json.loads(mkt_path.read_text()) + sectors = mkt.get("sectors", []) + + # 代码→名称映射(优先用本地缓存,避免每次跑都调akshare) + cache_path = DATA_DIR / "stock_name_code_cache.json" + name_to_code = {} + if cache_path.exists(): + name_to_code = json.loads(cache_path.read_text()) + if not name_to_code: + try: + import akshare as ak + df = ak.stock_info_a_code_name() + for _, r in df.iterrows(): + name_to_code[r["简称"]] = r["代码"] + cache_path.write_text(json.dumps(name_to_code, ensure_ascii=False)) + print(f" 名称代码映射: {len(name_to_code)}只已缓存", flush=True) + except Exception as e: + print(f" 名称代码映射加载失败: {e}", flush=True) + + leaders = [] + seen = set() + for s in sectors: + chg = s.get("change", 0) or 0 + lead_name = s.get("lead_stock", "") + if chg < SECTOR_HOT_THRESHOLD or not lead_name or lead_name in seen: + continue + seen.add(lead_name) + code = name_to_code.get(lead_name, "") + if not code: + continue + leaders.append({ + "code": code, + "name": lead_name, + "source": f"行业领涨-{s['name']}+{chg:+.1f}%", + }) + return leaders + except Exception as e: + print(f" 行业领涨获取失败: {e}", flush=True) + return [] + + def get_scanned_codes(conn): """取1小时内已扫描过的代码""" rows = conn.execute( @@ -192,10 +243,11 @@ def main(): # 1. 拉板 hot = fetch_hot_board() rotating, is_bullish = fetch_rotating_board() + leaders = fetch_sector_leaders() elapsed = time.time() - start_time - print(f"榜单: 东方财富{len(hot)}只, 同花顺{len(rotating)}只 ({elapsed:.0f}s)", flush=True) + print(f"榜单: 东方财富{len(hot)}只, 同花顺{len(rotating)}只, 行业领涨{len(leaders)}只 ({elapsed:.0f}s)", flush=True) - if not hot and not rotating: + if not hot and not rotating and not leaders: conn.close() return @@ -210,7 +262,8 @@ def main(): # 2. 合并去重 + 看空榜只保留持仓股 all_stocks = {} - for s in hot + rotating: + # 行业领涨优先(热门板块龙头) + for s in (leaders if is_bullish else []) + hot + rotating: code = s["code"] code_stripped = code.lstrip("0") if not is_bullish: