Initial commit
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user