Files
stock/stock-html/scan_history.py
T
freedakgmail 9c7d7abdd4 Initial commit
2026-07-17 18:49:35 +08:00

314 lines
12 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
"""
历史全景扫描回溯脚本 — 从指定日期开始,对每个交易日模拟 11:30 和 16:30 两次全市场扫描,
将推荐结果写入 stock_scan_history 表,供回测直接使用。
用法:
./venv/bin/python scan_history.py # 从 2026-01-01 扫描到今天
./venv/bin/python scan_history.py --start 2026-02-01 # 指定起始日
./venv/bin/python scan_history.py --end 2026-02-10 # 指定结束日
./venv/bin/python scan_history.py --force # 强制覆盖已扫描日期
说明:
11:30 扫描: 用 T-1 日 K 线 + T 日 open 模拟中午数据(对应回测 15:00 决策依据)
16:30 扫描: 用 T 日完整 K 线(对应回测次日 10:00 买入依据)
"""
import sys
import os
import time
import argparse
from datetime import datetime, date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import pandas as pd
import psycopg2
from psycopg2.extras import Json
from config import Config
from services.signal_detector import detect_all_signals
from services.stock_algorithms import compute_recommend
K_DAYS = 120
LOOKBACK = 5
SAVE_BATCH = 500
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_trading_days(conn, start: date, end: date):
with conn.cursor() as cur:
cur.execute("""
SELECT DISTINCT trade_date::date FROM stock_kline_daily
WHERE trade_date >= %s AND trade_date <= %s
ORDER BY trade_date
""", (start, end))
return [r[0] for r in cur.fetchall()]
def get_scanned_dates(conn, scan_time: str):
"""返回已扫描的日期集合"""
with conn.cursor() as cur:
cur.execute("""
SELECT DISTINCT scan_date FROM stock_scan_history
WHERE scan_time = %s
""", (scan_time,))
return {r[0] for r in cur.fetchall()}
def preload_all_klines(conn):
"""一次性加载全部 K 线到内存: {code: [(date,o,h,l,c,v), ...]}"""
print(" 加载全市场 K 线数据...", flush=True)
t0 = time.time()
result = {}
with conn.cursor() as cur:
cur.execute("""
SELECT code, trade_date, open, high, low, close, volume
FROM stock_kline_daily
ORDER BY code, trade_date
""")
buf_code = None
buf_rows = []
for r in cur:
code = r[0]
if code != buf_code:
if buf_code and buf_rows:
result[buf_code] = buf_rows
buf_code = code
buf_rows = []
buf_rows.append((str(r[1]), float(r[2]), float(r[3]), float(r[4]), float(r[5]), float(r[6])))
if buf_code and buf_rows:
result[buf_code] = buf_rows
print(f" 加载完成: {len(result)} 只股票, {time.time()-t0:.1f}s", flush=True)
return result
def build_df(rows, end_date_str: str, days: int = K_DAYS):
"""从预加载行构建 DataFrame(截止到 end_date_str"""
filtered = [r for r in rows if r[0] <= end_date_str]
if len(filtered) < 30:
return None
trimmed = filtered[-days:]
df = pd.DataFrame(trimmed, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
for col in ('open', 'high', 'low', 'close', 'volume'):
df[col] = df[col].astype(float)
return df
def build_noon_df(rows, day_t: date, ohlc_t: dict, code: str, days: int = K_DAYS):
"""构建 11:30 中午 K 线: T-1 前 + T 日 open"""
prev_str = str(day_t - timedelta(days=1))
filtered = [r for r in rows if r[0] <= prev_str]
if len(filtered) < 30:
return None
trimmed = filtered[-days:]
df = pd.DataFrame(trimmed, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
for col in ('open', 'high', 'low', 'close', 'volume'):
df[col] = df[col].astype(float)
if code in ohlc_t:
open_t = ohlc_t[code][0]
extra = pd.DataFrame([{
'date': str(day_t), 'open': open_t, 'high': open_t,
'low': open_t, 'close': open_t, 'volume': 0.0,
}])
return pd.concat([df, extra], ignore_index=True)
return df
def get_day_ohlc(conn, trade_date: date):
with conn.cursor() as cur:
cur.execute("""
SELECT code, open, close FROM stock_kline_daily WHERE trade_date = %s
""", (trade_date,))
return {r[0]: (float(r[1]), float(r[2])) for r in cur.fetchall()}
def scan_one_day(preloaded, codes, day_t, ohlc_t, scan_time, conn):
"""对指定日期的所有股票做一次扫描,返回结果列表。
scan_time='16:30': 用 T 日完整 K 线
scan_time='11:30': 用 T-1 + T 日 open 模拟中午
"""
results = []
day_str = str(day_t)
total = len(codes)
t0 = time.time()
for j, code in enumerate(codes):
if scan_time == '16:30':
df = build_df(preloaded.get(code, []), day_str, K_DAYS)
else:
df = build_noon_df(preloaded.get(code, []), day_t, ohlc_t, code, K_DAYS)
if df is None or len(df) < 30:
continue
try:
res = detect_all_signals(df, lookback=LOOKBACK)
except Exception:
continue
if res.get('error'):
continue
signal_status = res.get('signal_status', [])
indicators = res.get('indicators', {})
triggered_count = sum(1 for s in signal_status if s.get('triggered'))
is_holding = False
st, disp, reason, rate = compute_recommend(
signal_status, indicators, triggered_count, is_holding=is_holding
)
# 同时计算持仓版推荐(回测 15:00 需要两种)
st_h, disp_h, reason_h, rate_h = compute_recommend(
signal_status, indicators, triggered_count, is_holding=True
)
results.append({
'code': code,
'recommend_display': disp,
'recommend_type': st,
'recommend_reason': reason,
'recommend_rate': rate,
'triggered_count': triggered_count,
'signal_status': signal_status,
'indicators': indicators,
'disp_holding': disp_h,
'reason_holding': reason_h,
'rate_holding': rate_h,
})
if (j + 1) % 500 == 0:
elapsed = time.time() - t0
print(f" 已扫描 {j+1}/{total} {elapsed:.0f}s", flush=True)
return results
def save_results(conn, results, scan_date, scan_time):
"""批量写入扫描结果"""
if not results:
return
with conn.cursor() as cur:
for r in results:
# 将持仓版推荐也存入 indicators 字段方便回测
ind = r.get('indicators', {})
ind['_holding'] = {
'display': r.get('disp_holding', ''),
'reason': r.get('reason_holding', ''),
'rate': r.get('rate_holding', 0),
}
cur.execute("""
INSERT INTO stock_scan_history
(scan_date, scan_time, code, recommend_display, recommend_type,
recommend_reason, recommend_rate, triggered_count, signal_status, indicators)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (scan_date, scan_time, code) DO UPDATE SET
recommend_display = EXCLUDED.recommend_display,
recommend_type = EXCLUDED.recommend_type,
recommend_reason = EXCLUDED.recommend_reason,
recommend_rate = EXCLUDED.recommend_rate,
triggered_count = EXCLUDED.triggered_count,
signal_status = EXCLUDED.signal_status,
indicators = EXCLUDED.indicators,
created_at = CURRENT_TIMESTAMP
""", (
scan_date, scan_time, r['code'],
r['recommend_display'], r['recommend_type'],
r['recommend_reason'], r['recommend_rate'],
r['triggered_count'],
Json(r['signal_status']), Json(ind),
))
conn.commit()
def main():
parser = argparse.ArgumentParser(description='历史全景扫描回溯(11:30 + 16:30')
parser.add_argument('--start', type=str, default='2026-01-01', metavar='YYYY-MM-DD')
parser.add_argument('--end', type=str, default=None, metavar='YYYY-MM-DD')
parser.add_argument('--force', action='store_true', help='强制覆盖已扫描日期')
args = parser.parse_args()
start_date = datetime.strptime(args.start, '%Y-%m-%d').date()
end_date = datetime.strptime(args.end, '%Y-%m-%d').date() if args.end else date.today()
conn = get_db_conn()
print("=" * 70)
print(" 历史全景扫描回溯")
print("=" * 70)
print(f" 扫描区间: {start_date} ~ {end_date}")
print(f" 扫描时段: 11:30(中午)+ 16:30(收盘后)")
print(f" 强制覆盖: {'' if args.force else '否(跳过已扫描日期)'}")
print("-" * 70)
trading_days = get_trading_days(conn, start_date, end_date)
if not trading_days:
print("错误: 无交易日数据")
conn.close()
return
print(f" 交易日数: {len(trading_days)}")
scanned_1130 = get_scanned_dates(conn, '11:30') if not args.force else set()
scanned_1630 = get_scanned_dates(conn, '16:30') if not args.force else set()
preloaded = preload_all_klines(conn)
all_codes = sorted(preloaded.keys())
print(f" 可扫描股票: {len(all_codes)}")
print("=" * 70)
total_start = time.time()
total_scans = 0
for i, day_t in enumerate(trading_days):
need_1130 = day_t not in scanned_1130
need_1630 = day_t not in scanned_1630
if not need_1130 and not need_1630:
continue
ohlc_t = get_day_ohlc(conn, day_t)
if not ohlc_t:
continue
print(f"\n [{i+1}/{len(trading_days)}] {day_t}", flush=True)
# 16:30 收盘后扫描(用 T 日完整 K 线)
if need_1630:
t0 = time.time()
print(f" 16:30 扫描中...", flush=True)
results = scan_one_day(preloaded, all_codes, day_t, ohlc_t, '16:30', conn)
save_results(conn, results, day_t, '16:30')
buy_count = sum(1 for r in results if r['recommend_display'] == '买入')
sell_count = sum(1 for r in results if r['recommend_display'] == '卖出')
print(f" 16:30 完成: {len(results)} 只 买入推荐 {buy_count} 卖出 {sell_count} {time.time()-t0:.0f}s",
flush=True)
total_scans += 1
# 11:30 中午扫描(用 T-1 + T 日 open 模拟)
if need_1130:
t0 = time.time()
print(f" 11:30 扫描中...", flush=True)
results = scan_one_day(preloaded, all_codes, day_t, ohlc_t, '11:30', conn)
save_results(conn, results, day_t, '11:30')
buy_count = sum(1 for r in results if r['recommend_display'] == '买入')
sell_count = sum(1 for r in results if r['recommend_display'] == '卖出')
print(f" 11:30 完成: {len(results)} 只 买入推荐 {buy_count} 卖出 {sell_count} {time.time()-t0:.0f}s",
flush=True)
total_scans += 1
elapsed = time.time() - total_start
print(f"\n{'=' * 70}")
print(f" 全部完成!")
print(f" 扫描次数: {total_scans} 次({len(trading_days)}× 2 时段)")
print(f" 总耗时 : {elapsed/60:.1f} 分钟")
print(f"{'=' * 70}")
conn.close()
if __name__ == '__main__':
main()