fix: xiaoguo_scanner 榜单更新+看空榜持仓预警
- 修bug:stock_rank_cxd_ths 实为'创新低',改为 stock_rank_lxsz_ths '连续上涨' - 新增6个看多榜(险资举牌)+ 5个看空榜(创新低/持续缩量/量价齐跌/连续下跌/向下突破) - 看空榜自动比对持仓,命中写入 xiaoguo_risk 信号 - 东财热榜静默降级(502不可修) - 看空榜不跳过已扫描,每轮全检
This commit is contained in:
+87
-34
@@ -23,15 +23,28 @@ SCAN_INTERVAL = 3600 # 同一只股1小时内不重复搜
|
||||
MAX_STOCKS_PER_RUN = 15
|
||||
ARTICLES_PER_STOCK = 3
|
||||
|
||||
# 同花顺轮流榜
|
||||
ROTATING_BOARDS = [
|
||||
# 同花顺看多榜(挖掘潜力股)
|
||||
BULLISH_BOARDS = [
|
||||
("创新高", "stock_rank_cxg_ths"),
|
||||
("量价齐升", "stock_rank_ljqs_ths"),
|
||||
("向上突破", "stock_rank_xstp_ths"),
|
||||
("连续上涨", "stock_rank_cxd_ths"),
|
||||
("连续放量", "stock_rank_cxfl_ths"),
|
||||
("连续上涨", "stock_rank_lxsz_ths"),
|
||||
("持续放量", "stock_rank_cxfl_ths"),
|
||||
("险资举牌", "stock_rank_xzjp_ths"),
|
||||
]
|
||||
|
||||
# 同花顺看空榜(持仓风险预警)
|
||||
BEARISH_BOARDS = [
|
||||
("创新低", "stock_rank_cxd_ths"),
|
||||
("持续缩量", "stock_rank_cxsl_ths"),
|
||||
("量价齐跌", "stock_rank_ljqd_ths"),
|
||||
("连续下跌", "stock_rank_lxxd_ths"),
|
||||
("向下突破", "stock_rank_xxtp_ths"),
|
||||
]
|
||||
|
||||
ALL_BOARDS = BULLISH_BOARDS + BEARISH_BOARDS
|
||||
BULLISH_COUNT = len(BULLISH_BOARDS)
|
||||
|
||||
|
||||
def clean_proxy():
|
||||
for k in ['http_proxy','https_proxy','HTTP_PROXY','HTTPS_PROXY']:
|
||||
@@ -63,25 +76,26 @@ def fetch_hot_board():
|
||||
return [{"code": str(r[code_col]).zfill(6).strip(), "name": str(r[name_col]).strip(),
|
||||
"rank": i+1, "source": "东方财富热榜"}
|
||||
for i, (_, r) in enumerate(df.head(30).iterrows())]
|
||||
except Exception as e:
|
||||
print(f" 热榜失败: {e}", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def fetch_rotating_board():
|
||||
"""同花顺轮流榜(每轮一个)"""
|
||||
"""同花顺轮流榜(每轮一个),返回 (股票列表, 是否看多)"""
|
||||
if not HAS_AKSHARE:
|
||||
return []
|
||||
return [], True
|
||||
conn = get_conn()
|
||||
row = conn.execute("SELECT val FROM state_meta WHERE key='xiaoguo_board_round'").fetchone()
|
||||
round_idx = (int(row[0]) if row else 0) % len(ROTATING_BOARDS)
|
||||
round_idx = (int(row[0]) if row else 0) % len(ALL_BOARDS)
|
||||
conn.execute("INSERT OR REPLACE INTO state_meta (key, val) VALUES ('xiaoguo_board_round', ?)",
|
||||
(str((round_idx + 1) % len(ROTATING_BOARDS)),))
|
||||
(str((round_idx + 1) % len(ALL_BOARDS)),))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
board_name, func_name = ROTATING_BOARDS[round_idx]
|
||||
print(f" 同花顺榜: {board_name}", flush=True)
|
||||
board_name, func_name = ALL_BOARDS[round_idx]
|
||||
is_bullish = round_idx < BULLISH_COUNT
|
||||
print(f" 同花顺榜: {board_name} {'📈看多' if is_bullish else '📉看空'}", flush=True)
|
||||
|
||||
try:
|
||||
clean_proxy()
|
||||
@@ -92,10 +106,10 @@ def fetch_rotating_board():
|
||||
name_col = [c for c in cols if '简称' in c or '名称' in c][0]
|
||||
return [{"code": str(r[code_col]).zfill(6), "name": str(r[name_col]).strip(),
|
||||
"source": f"同花顺{board_name}"}
|
||||
for _, r in df.head(15).iterrows()]
|
||||
for _, r in df.head(15).iterrows()], is_bullish
|
||||
except Exception as e:
|
||||
print(f" {board_name}失败: {e}", flush=True)
|
||||
return []
|
||||
return [], is_bullish
|
||||
|
||||
|
||||
def get_scanned_codes(conn):
|
||||
@@ -175,9 +189,9 @@ def main():
|
||||
start_time = time.time()
|
||||
conn = get_conn()
|
||||
|
||||
# 1. 拉榜
|
||||
# 1. 拉板
|
||||
hot = fetch_hot_board()
|
||||
rotating = fetch_rotating_board()
|
||||
rotating, is_bullish = fetch_rotating_board()
|
||||
elapsed = time.time() - start_time
|
||||
print(f"榜单: 东方财富{len(hot)}只, 同花顺{len(rotating)}只 ({elapsed:.0f}s)", flush=True)
|
||||
|
||||
@@ -185,56 +199,95 @@ def main():
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# 2. 合并去重
|
||||
# 加载持仓代码(用于看空榜比对)
|
||||
holdings = set()
|
||||
if not is_bullish:
|
||||
cur = conn.execute("SELECT code FROM holdings WHERE is_active=1")
|
||||
holdings = {r[0].lstrip("0") for r in cur.fetchall()}
|
||||
# 也查自选
|
||||
cur2 = conn.execute("SELECT code FROM watchlist_stocks")
|
||||
holdings.update({r[0].lstrip("0") for r in cur2.fetchall()})
|
||||
|
||||
# 2. 合并去重 + 看空榜只保留持仓股
|
||||
all_stocks = {}
|
||||
for s in hot + rotating:
|
||||
code = s["code"]
|
||||
code_stripped = code.lstrip("0")
|
||||
if not is_bullish:
|
||||
# 看空榜:只处理持仓/自选中的股票
|
||||
if code_stripped not in holdings:
|
||||
continue
|
||||
if code not in all_stocks:
|
||||
all_stocks[code] = {"code": code, "name": s["name"], "sources": []}
|
||||
all_stocks[code]["sources"].append(s["source"])
|
||||
|
||||
# 3. 排除已搜索过的
|
||||
if not all_stocks:
|
||||
if is_bullish:
|
||||
print("榜单为空", flush=True)
|
||||
else:
|
||||
print(f"看空榜无持仓股命中", flush=True)
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# 3. 排除已搜索过的(看空榜不排除——每次都要检查风险)
|
||||
scanned = get_scanned_codes(conn)
|
||||
candidates = [s for code, s in all_stocks.items()
|
||||
if code not in scanned and len(code) == 6 and code.isdigit()][:MAX_STOCKS_PER_RUN]
|
||||
if is_bullish:
|
||||
candidates = [s for code, s in all_stocks.items()
|
||||
if code not in scanned and len(code) == 6 and code.isdigit()][:MAX_STOCKS_PER_RUN]
|
||||
else:
|
||||
# 看空榜:不限数量,全检
|
||||
candidates = [s for code, s in all_stocks.items()
|
||||
if len(code) == 6 and code.isdigit()]
|
||||
|
||||
if not candidates:
|
||||
print(f"无新候选(已有 {len(scanned)} 只已扫描)", flush=True)
|
||||
conn.close()
|
||||
return
|
||||
|
||||
print(f"待扫描: {len(candidates)} 只(跳过 {len(all_stocks)-len(candidates)} 只已扫过)", flush=True)
|
||||
print(f"待扫描: {len(candidates)} 只({'看多' if is_bullish else '看空'}榜)", flush=True)
|
||||
|
||||
# 4. 逐只搜新闻+判断
|
||||
# 4. 逐只处理
|
||||
found_any = False
|
||||
for stock in candidates:
|
||||
code, name = stock["code"], stock["name"]
|
||||
sources = "|".join(stock["sources"])
|
||||
|
||||
articles = search_news(code, ARTICLES_PER_STOCK)
|
||||
if not articles:
|
||||
articles = search_news(code, ARTICLES_PER_STOCK) if is_bullish else []
|
||||
if not articles and is_bullish:
|
||||
mark_scanned(conn, code, name, False)
|
||||
continue
|
||||
|
||||
has_found = False
|
||||
ok, sentiment = check_stock(code, name, articles)
|
||||
if ok:
|
||||
if is_bullish:
|
||||
ok, sentiment = check_stock(code, name, articles)
|
||||
if ok:
|
||||
has_found = True
|
||||
found_any = True
|
||||
conn.execute(
|
||||
"INSERT INTO signal_news (signal_id, sector, overall_sentiment, summary, key_articles, searched_stocks, source) "
|
||||
"VALUES (NULL, ?, ?, ?, ?, ?, 'xiaoguo')",
|
||||
(f"扫描-{name}", sentiment, f"[{sources}] {articles[0]['title'][:80]}",
|
||||
json.dumps([{"title": a["title"], "sentiment": sentiment, "summary": (a.get("content") or "")[:100]} for a in articles[:3]], ensure_ascii=False),
|
||||
json.dumps([name], ensure_ascii=False))
|
||||
)
|
||||
print(f" ✅ {name}({code}) [{sources}] {sentiment}: {articles[0]['title'][:50]}", flush=True)
|
||||
mark_scanned(conn, code, name, has_found)
|
||||
else:
|
||||
# 看空榜:直接写入风险信号
|
||||
has_found = True
|
||||
found_any = True
|
||||
sources = "|".join(stock["sources"])
|
||||
conn.execute(
|
||||
"INSERT INTO signal_news (signal_id, sector, overall_sentiment, summary, key_articles, searched_stocks, source) "
|
||||
"VALUES (NULL, ?, ?, ?, ?, ?, 'xiaoguo')",
|
||||
(f"扫描-{name}", sentiment, f"[{sources}] {articles[0]['title'][:80]}",
|
||||
json.dumps([{"title": a["title"], "sentiment": sentiment, "summary": (a.get("content") or "")[:100]} for a in articles[:3]], ensure_ascii=False),
|
||||
"VALUES (NULL, ?, ?, ?, ?, ?, 'xiaoguo_risk')",
|
||||
(f"预警-{name}", "偏空", f"[{sources}] {name}登上{sources}榜,需关注持仓风险",
|
||||
json.dumps([{"title": name, "sentiment": "偏空", "summary": f"上榜{sources}"}], ensure_ascii=False),
|
||||
json.dumps([name], ensure_ascii=False))
|
||||
)
|
||||
print(f" ✅ {name}({code}) [{sources}] {sentiment}: {articles[0]['title'][:50]}", flush=True)
|
||||
|
||||
mark_scanned(conn, code, name, has_found)
|
||||
print(f" ⚠️ {name}({code}) [{sources}] 持仓风险信号", flush=True)
|
||||
mark_scanned(conn, code, name, has_found)
|
||||
|
||||
total_time = time.time() - start_time
|
||||
print(f"完成: {len(candidates)}只扫描, {'有发现' if found_any else '无发现'} ({total_time:.0f}s)", flush=True)
|
||||
print(f"完成: {len(candidates)}只{'看多' if is_bullish else '看空'}扫描, {'有发现' if found_any else '无发现'} ({total_time:.0f}s)", flush=True)
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user