Files
stock/stock-html/sync_kline.py
T
selfrelease 2b5a32ca1e feat: 新增外部因素分析模块+综合评分引擎+算法文档重构
新增模块:
- fund_flow_analyzer.py: 主力资金流向分析(P0, ±20)
- market_sentiment.py: 市场情绪指标(P1, ±10)
- external_factors.py: 北向资金/美股/大宗商品/汇率(P2-P4,P7)
- news_analyzer.py: 公告/并购/政策面LLM分析(P5-P6)
- score_engine.py: 综合评分引擎,整合技术面+外部因素

路由更新:
- analysis.py: deep_analyze接入综合评分,根据最终评级修正买卖建议
- market.py: 新增4个外部因素API端点
- trades.py: 交易路由更新

算法文档重构:
- 章节重排: 技术面(二三)→外部因素(四)→买卖决策(五)→数据源(六)→性能(七)
- 架构图更新为五层,标注章节对应
- 5.1/5.2标注纯技术面,5.3整合外部因素修正推荐
2026-07-17 23:50:42 +08:00

215 lines
6.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
"""
K线数据本地同步脚本
支持四种数据源(按优先级):阿里云API → 新浪API → 麦蕊API → AKShare
北交所(8XX/9XX)股票专用新浪API
同步到本地PostgreSQL数据库
用法:
python sync_kline.py # 增量同步(默认,只同步最近5天)
python sync_kline.py --full # 全量同步(180天历史数据)
python sync_kline.py --days 30 # 同步最近30天
"""
import sys
import os
import time
import argparse
import signal as sig_module
from datetime import datetime, date, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import psycopg2
from psycopg2.extras import execute_values
from config import Config
from services.stock_algorithms import fetch_kline_rows
# ============ 配置 ============
WORKERS = 10 # 并发线程数
BATCH_SAVE_SIZE = 50 # 每批保存到DB的股票数
FULL_SYNC_DAYS = 180 # 全量同步天数
INCREMENTAL_DAYS = 5 # 增量同步天数(多取几天防遗漏)
_shutdown = False
def signal_handler(signum, frame):
global _shutdown
print("\n⚠️ 收到中断信号,正在优雅退出...")
_shutdown = True
sig_module.signal(sig_module.SIGINT, signal_handler)
sig_module.signal(sig_module.SIGTERM, signal_handler)
def get_db_conn():
return psycopg2.connect(
host=Config.DB_HOST, port=Config.DB_PORT,
dbname=Config.DB_NAME, user=Config.DB_USER, password=Config.DB_PASSWORD,
)
def get_all_stock_codes(conn):
"""获取可交易的股票列表(排除退市、停牌等无效股票)"""
with conn.cursor() as cur:
cur.execute("""
SELECT code, name FROM stock_realtime_price
WHERE volume > 0 AND price > 0
AND name NOT LIKE '%%退%%'
AND name NOT LIKE 'PT%%'
ORDER BY code
""")
return cur.fetchall()
def fetch_kline_for_stock(code, days):
"""获取K线数据 — 委托给 services.stock_algorithms.fetch_kline_rows"""
return fetch_kline_rows(code, days)
def save_kline_batch(conn, all_rows):
"""批量保存K线数据到数据库(UPSERT)"""
if not all_rows:
return 0
with conn.cursor() as cur:
execute_values(
cur,
"""
INSERT INTO stock_kline_daily (code, trade_date, open, high, low, close, volume, amount)
VALUES %s
ON CONFLICT (code, trade_date) DO UPDATE SET
open = EXCLUDED.open,
high = EXCLUDED.high,
low = EXCLUDED.low,
close = EXCLUDED.close,
volume = EXCLUDED.volume,
amount = EXCLUDED.amount,
updated_at = CURRENT_TIMESTAMP
""",
all_rows,
page_size=1000,
)
conn.commit()
return len(all_rows)
def main():
global _shutdown
parser = argparse.ArgumentParser(description='K线数据本地同步')
parser.add_argument('--full', action='store_true', help='全量同步(180天历史)')
parser.add_argument('--days', type=int, default=None, help='同步天数')
args = parser.parse_args()
if args.full:
sync_days = FULL_SYNC_DAYS
mode = '全量同步'
elif args.days:
sync_days = args.days
mode = f'自定义同步({sync_days}天)'
else:
sync_days = INCREMENTAL_DAYS
mode = '增量同步'
print(f"{'='*60}", flush=True)
print(f"📊 K线数据本地同步", flush=True)
print(f"📅 日期: {date.today()}", flush=True)
print(f"🔄 模式: {mode}{sync_days}天)", flush=True)
print(f"⚙️ 并发数: {WORKERS}", flush=True)
print(f"{'='*60}", flush=True)
conn = get_db_conn()
all_stocks = get_all_stock_codes(conn)
total = len(all_stocks)
print(f"📈 股票总数: {total}", flush=True)
start_time = time.time()
done_count = 0
success_count = 0
error_count = 0
total_rows = 0
save_buffer = []
last_report_time = time.time()
print(f"\n🚀 开始同步...", flush=True)
print(f"-" * 60, flush=True)
with ThreadPoolExecutor(max_workers=WORKERS) as executor:
futures = {}
for code, name in all_stocks:
if _shutdown:
break
futures[executor.submit(fetch_kline_for_stock, code, sync_days)] = (code, name)
for future in as_completed(futures):
if _shutdown:
print("⏹️ 用户中断,正在保存当前数据...", flush=True)
break
code, name = futures[future]
done_count += 1
try:
rows = future.result()
except Exception:
rows = None
if rows:
save_buffer.extend(rows)
success_count += 1
else:
error_count += 1
# 攒够一批就保存
if len(save_buffer) >= BATCH_SAVE_SIZE * 80: # 约50股 × 80条/股
saved = save_kline_batch(conn, save_buffer)
total_rows += saved
save_buffer = []
# 每3秒报告进度
now = time.time()
if now - last_report_time >= 3:
elapsed = now - start_time
speed = done_count / elapsed if elapsed > 0 else 0
remaining = (total - done_count) / speed if speed > 0 else 0
pct = done_count / total * 100
print(f" [{pct:5.1f}%] {done_count}/{total} "
f"| 速度: {speed:.1f}只/秒 | 剩余: {remaining/60:.1f}分钟 "
f"| 成功: {success_count} | 失败: {error_count} "
f"| 已保存: {total_rows}", flush=True)
last_report_time = now
# 保存剩余数据
if save_buffer:
saved = save_kline_batch(conn, save_buffer)
total_rows += saved
elapsed = time.time() - start_time
speed = done_count / elapsed if elapsed > 0 else 0
print(f"\n{'='*60}", flush=True)
print(f"✅ 同步{'中断' if _shutdown else '完成'}", flush=True)
print(f" 处理: {done_count} 只 | 成功: {success_count} | 失败: {error_count}", flush=True)
print(f" 保存K线: {total_rows} 条 | 耗时: {elapsed/60:.1f}分钟", flush=True)
print(f" 平均速度: {speed:.1f} 只/秒", flush=True)
# 显示数据库统计
with conn.cursor() as cur:
cur.execute("SELECT count(*), count(DISTINCT code), min(trade_date), max(trade_date) FROM stock_kline_daily")
cnt, codes, min_date, max_date = cur.fetchone()
print(f"\n📊 数据库K线统计:", flush=True)
print(f" 总记录: {cnt:,} 条 | 覆盖股票: {codes}", flush=True)
print(f" 日期范围: {min_date} ~ {max_date}", flush=True)
print(f"{'='*60}", flush=True)
conn.close()
if __name__ == '__main__':
main()