# -*- coding: utf-8 -*- """ 量化交易监控主程序 功能: 1. 循环抓取美股数据 (支持全量/Top N) 2. 调用分析模块进行分析 3. 调用交易模块执行信号 """ import time import argparse from logging_setup import init_logging from futu import StockDataIntegrator, EastMoneyAPI from market_analyzer import MarketAnalyzer from trader import Trader from datetime import datetime from data_writer import write_symbols, append_bars_1m, append_bars_session, append_signals, append_features_1m, append_etl_run from zoneinfo import ZoneInfo from utils_time import now_et, fmt_et, fmt_et_hm from signal_filter import SignalCooldownFilter def main(): # 初始化简单日志(保持现有 print,不强制替换) init_logging() parser = argparse.ArgumentParser(description='AI量化交易监控系统') parser.add_argument('--interval', type=int, default=60, help='监控间隔(秒)') parser.add_argument('--limit', type=int, default=100, help='每次监控的股票数量') parser.add_argument('--all', action='store_true', help='监控所有股票(速度较慢)') parser.add_argument('--premarket', action='store_true', help='在盘前窗口抓取富途盘前价格并写入 session=pre') parser.add_argument('--premarket-limit', type=int, default=30, help='盘前抓取的最大股票数(富途页面逐个抓取)') parser.add_argument('--session-override', choices=['pre','regular','post'], help='测试用手动覆盖当前交易时段') args = parser.parse_args() print("🚀 启动 AI 量化交易监控系统...") print(f"⏱️ 监控间隔: {args.interval} 秒") # 初始化模块 integrator = StockDataIntegrator() eastmoney_api = EastMoneyAPI() # 用于快速获取列表 analyzer = MarketAnalyzer() trader = Trader() cooldown_filter = SignalCooldownFilter(cooldown_minutes=30) loop_count = 0 try: while True: loop_start = now_et() loop_count += 1 print(f"\n🔄 第 {loop_count} 次扫描开始 - {fmt_et_hm() } ET") # 1. 抓取数据 (常规东方财富列表) # 为了监控效率,我们主要使用东方财富的快速列表接口 # 如果是全量监控 stock_data = [] if args.all: print("📡 正在获取全量市场数据...") # 这里我们简化处理,直接调用修改后的API获取所有数据 # 注意:futu.py 中的 get_us_stocks 已经支持分页获取 # 为了演示,我们这里只获取前几页,或者使用 futu.py 中新增的逻辑 # 直接使用 integrator 的逻辑,但强制 eastmoney_only 以提高速度 # 我们手动调用 eastmoney_api 来获取数据,避免 integrator 的复杂逻辑 # 获取所有数据可能需要一点时间 _, total = eastmoney_api.get_us_stocks(page_size=1) print(f"📊 市场总股票数: {total}") # 分页获取所有数据 page_size = 100 limit = total total_pages = (limit + page_size - 1) // page_size all_raw_stocks = [] for page in range(1, total_pages + 1): stocks, _ = eastmoney_api.get_us_stocks(page_size=page_size, page_index=page) if stocks: all_raw_stocks.extend(stocks) # 稍微延时 # time.sleep(0.1) # 解析数据 for item in all_raw_stocks: parsed = eastmoney_api.parse_stock_data(item) if parsed: stock_data.append({ 'symbol': parsed['symbol'], 'name': parsed['name'], 'eastmoney_price': parsed['current_price'], 'eastmoney_change_ratio': parsed['change_ratio'] }) else: print(f"📡 正在获取 Top {args.limit} 热门股票数据...") # 获取 Top N raw_stocks, _ = eastmoney_api.get_us_stocks(page_size=args.limit) for item in raw_stocks: parsed = eastmoney_api.parse_stock_data(item) if parsed: stock_data.append({ 'symbol': parsed['symbol'], 'name': parsed['name'], 'eastmoney_price': parsed['current_price'], 'eastmoney_change_ratio': parsed['change_ratio'] }) print(f"✅ 获取到 {len(stock_data)} 条有效常规行情数据 (ET {fmt_et_hm()})") # 1.1 盘前数据补充 (仅在盘前窗口且开启参数时,对前 N 只股票抓取富途页面) def _get_us_market_session(now_et: datetime) -> str: """根据美东时间判定交易时段: pre(4:00-9:30), regular(9:30-16:00), post(16:00-20:00), off 其它。 周末直接 off。夏令时由系统 tz 数据自动处理。""" if now_et.weekday() >= 5: # Saturday=5 Sunday=6 return 'off' minutes = now_et.hour * 60 + now_et.minute if 4*60 <= minutes < 9*60 + 30: return 'pre' if 9*60 + 30 <= minutes < 16*60: return 'regular' if 16*60 <= minutes < 20*60: return 'post' return 'off' def _current_session() -> str: if args.session_override: return args.session_override now_et = datetime.now(ZoneInfo('America/New_York')) return _get_us_market_session(now_et) pre_rows = [] session = _current_session() if args.premarket and session == 'pre': pre_candidates = stock_data[: args.premarket_limit] print(f"🌙 盘前窗口内,准备抓取富途盘前数据 {len(pre_candidates)} 条... (ET {fmt_et_hm()})") for i, item in enumerate(pre_candidates, 1): symbol = item['symbol'] futu_detail = integrator.get_futu_stock_details(symbol) if futu_detail and futu_detail.get('before_open_price'): # 正常化盘前涨跌幅 (可能含 %) ratio_raw = futu_detail.get('before_open_change_ratio') or '' ratio_val = 0.0 try: ratio_clean = str(ratio_raw).replace('%','').strip() if ratio_clean: ratio_f = float(ratio_clean) # 转为小数 ratio_val = ratio_f/100.0 except Exception: ratio_val = 0.0 item.update({ 'premarket_price': futu_detail.get('before_open_price'), 'premarket_change': futu_detail.get('before_open_change'), 'premarket_change_ratio': ratio_val, 'futu_before_open_price': futu_detail.get('before_open_price'), # 兼容 append_bars_session fallback }) pre_rows.append(item) if i % 10 == 0: print(f"🌙 盘前抓取进度 {i}/{len(pre_candidates)} (ET {fmt_et_hm()})") print(f"🌙 富途盘前成功获取 {len(pre_rows)} 条 (ET {fmt_et_hm()})") else: if args.premarket: print(f"🌙 当前交易时段为 {session},未执行盘前抓取 (ET {fmt_et_hm()})") # 2.1 将 symbols 与 1分钟线写入 CSV(data/ 下) try: symbol_id_map = write_symbols([ { 'symbol': s['symbol'], 'name': s['name'], 'exchange': 'US', 'currency': 'USD', } for s in stock_data ]) new_rows = append_bars_1m(stock_data, symbol_id_map, source='eastmoney') append_features_1m(new_rows) # 盘前 bars 写入 (不计算特征,避免与常规混淆) if pre_rows: append_bars_session(pre_rows, symbol_id_map, source='futu', session='pre') except Exception as e: print(f"⚠️ 数据落地失败: {e}") # 3. 分析数据 raw_signals = analyzer.analyze(stock_data) # 盘前可选:根据盘前涨幅单独生成预警信号(示例阈值 +3% / -3%) premarket_signals = [] if pre_rows: for r in pre_rows: ratio = r.get('premarket_change_ratio') or 0.0 sym = r['symbol'] name = r.get('name', '') price = r.get('premarket_price') or r.get('eastmoney_price') if ratio >= 0.03: premarket_signals.append({'symbol': sym, 'name': name, 'price': price, 'type': 'BUY', 'reason': f'盘前涨幅 {ratio:.2%} 预警'}) elif ratio <= -0.03: premarket_signals.append({'symbol': sym, 'name': name, 'price': price, 'type': 'SELL', 'reason': f'盘前跌幅 {ratio:.2%} 预警'}) if premarket_signals: print(f"🌙 盘前预警信号 {len(premarket_signals)} 条 (ET {fmt_et_hm()})") raw_signals.extend(premarket_signals) signals = cooldown_filter.filter(raw_signals) # 3.1 写入 signals.csv try: if signals: append_signals(signals, symbol_id_map) except Exception as e: print(f"⚠️ 写入信号失败: {e}") # 4. 执行交易 if signals: trader.execute_signals(signals) else: print("💤 当前无交易信号") # 等待下一次扫描 # 记录ETL运行 try: duration = (now_et() - loop_start).total_seconds() append_etl_run(loop_count, len(stock_data), len(signals), duration, errors=0) except Exception as e: print(f"⚠️ 记录ETL统计失败: {e}") print(f"⏳ 等待 {args.interval} 秒...") time.sleep(args.interval) except KeyboardInterrupt: print("\n🛑 监控已停止") if __name__ == "__main__": main()