""" BTC Hourly 自动交易脚本 基于DLinear、N-BEATS、TTM-R2多模型集成预测,在Polymarket自动下单BTC小时涨跌 支持: - 每小时自动寻找对应市场 - 实时价格刷新 - 定时下单 - 详细日志记录 """ import os import sys import argparse import json import time import logging from logging.handlers import RotatingFileHandler import schedule from datetime import datetime, timedelta from typing import Optional, Tuple, Dict, List from zoneinfo import ZoneInfo import torch import numpy as np import pandas as pd import httpx from dotenv import load_dotenv # 添加项目路径 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from data.fetcher import BinanceDataFetcher from data.trading_db import TradingDatabase from api.models import PredictionModel from models.inference import get_inference_manager load_dotenv() def setup_logging(log_dir: str = "logs", log_level: int = logging.INFO) -> logging.Logger: """ 配置日志系统 Args: log_dir: 日志目录 log_level: 日志级别 Returns: 配置好的logger """ # 创建日志目录 os.makedirs(log_dir, exist_ok=True) # 创建logger logger = logging.getLogger("polymarket_trader") logger.setLevel(log_level) logger.propagate = False # 避免重复添加handler if logger.handlers: return logger # 日志格式 formatter = logging.Formatter( "%(asctime)s | %(levelname)-8s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) # 文件handler (按大小轮转, 最大10MB, 保留5个备份) log_file = os.path.join(log_dir, "polymarket_trading.log") file_handler = RotatingFileHandler( log_file, maxBytes=10*1024*1024, # 10MB backupCount=5, encoding='utf-8' ) file_handler.setLevel(log_level) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # 控制台handler console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(log_level) console_handler.setFormatter(formatter) logger.addHandler(console_handler) return logger # 初始化全局logger logger = setup_logging() class PolymarketBTCTrader: """Polymarket BTC Hourly交易器 (多模型集成版)""" GAMMA_API = "https://gamma-api.polymarket.com" CLOB_API = "https://clob.polymarket.com" # 月份名称映射 MONTH_NAMES = { 1: 'january', 2: 'february', 3: 'march', 4: 'april', 5: 'may', 6: 'june', 7: 'july', 8: 'august', 9: 'september', 10: 'october', 11: 'november', 12: 'december' } def __init__( self, private_key: Optional[str] = None, max_bet: float = 1.0, max_price: float = 0.50, dry_run: bool = True, ): self.private_key = private_key or os.getenv("POLYMARKET_PRIVATE_KEY") self.max_bet = max_bet self.max_price = max_price self.dry_run = dry_run # HTTP客户端 proxy = os.getenv("SOCKS5_PROXY") if os.getenv("USE_PROXY", "false").lower() == "true" else None self.http = httpx.Client( timeout=30.0, proxy=proxy, headers={'Accept-Encoding': 'identity'} ) # 初始化推理管理器 logger.info("初始化推理管理器...") self.inference_manager = get_inference_manager() self.inference_manager.load_all_models() self.device = self.inference_manager.device # 数据获取器 self.fetcher = BinanceDataFetcher() # 数据库 self.db = TradingDatabase() # CLOB客户端(用于下单) self.clob_client = None if self.private_key and not self.dry_run: self._init_clob_client() # 当前市场缓存 self.current_market = None self.last_price_update = None def _init_clob_client(self): """初始化CLOB交易客户端""" try: from py_clob_client.client import ClobClient self.clob_client = ClobClient( host=self.CLOB_API, key=self.private_key, chain_id=137, # Polygon ) self.clob_client.set_api_creds(self.clob_client.create_or_derive_api_creds()) logger.info("✓ CLOB客户端初始化成功") except Exception as e: logger.warning(f"⚠️ CLOB客户端初始化失败: {e}") self.clob_client = None def _log_to_db(self, result: Dict, market: Optional[Dict]): """记录交易结果到数据库""" try: # 确定状态 status = 'failure' if self.dry_run: status = 'test' elif result.get('success'): status = 'success' elif result.get('error'): status = 'failure' elif result.get('prediction'): # 有预测但未交易(如市场未找到) status = 'in_progress' if result.get('error'): status = 'failure' log_data = { 'timestamp': result.get('timestamp'), 'market_title': result.get('market'), 'market_slug': market.get('slug') if market else result.get('market_slug'), 'model_name': 'Ensemble', # 记录为集成策略 'prediction': result.get('prediction'), 'probability': result.get('probability'), 'action': result.get('side'), 'price': result.get('price'), 'amount': result.get('amount'), 'size': result.get('size'), 'status': status, 'error_message': result.get('error'), 'raw_data': result } self.db.log_trade(log_data) logger.info(f"💾 交易记录已保存到数据库") except Exception as e: logger.error(f"❌ 保存数据库失败: {e}") def generate_market_slug(self, target_time: Optional[datetime] = None) -> str: """ 生成市场slug 格式: bitcoin-up-or-down-{month}-{day}-{hour}am/pm-et 例如: bitcoin-up-or-down-february-3-8am-et """ # 获取美东时间 et_tz = ZoneInfo('America/New_York') if target_time is None: now_et = datetime.now(et_tz) target_time = now_et.replace(minute=0, second=0, microsecond=0) month_name = self.MONTH_NAMES[target_time.month] day = target_time.day # 转换为12小时制 hour_24 = target_time.hour if hour_24 == 0: hour_12 = 12 am_pm = 'am' elif hour_24 < 12: hour_12 = hour_24 am_pm = 'am' elif hour_24 == 12: hour_12 = 12 am_pm = 'pm' else: hour_12 = hour_24 - 12 am_pm = 'pm' slug = f"bitcoin-up-or-down-{month_name}-{day}-{hour_12}{am_pm}-et" return slug def get_market_by_slug(self, slug: str) -> Optional[Dict]: """通过slug获取市场信息""" try: response = self.http.get( f"{self.GAMMA_API}/events", params={'slug': slug} ) response.raise_for_status() events = response.json() if not events: return None event = events[0] markets = event.get('markets', []) if not markets: return None market = markets[0] # outcomePrices可能是JSON字符串或列表 outcome_prices_raw = market.get('outcomePrices', ['0', '0']) if isinstance(outcome_prices_raw, str): import json as _json try: outcome_prices_raw = _json.loads(outcome_prices_raw) except: outcome_prices_raw = ['0', '0'] # clobTokenIds也可能是JSON字符串 clob_tokens_raw = market.get('clobTokenIds', []) if isinstance(clob_tokens_raw, str): import json as _json try: clob_tokens_raw = _json.loads(clob_tokens_raw) except: clob_tokens_raw = [] return { 'title': event.get('title', ''), 'slug': slug, 'question': market.get('question', ''), 'condition_id': market.get('conditionId', ''), 'outcomes': market.get('outcomes', []), 'outcome_prices': [float(p) for p in outcome_prices_raw], 'tokens': clob_tokens_raw, 'end_date': event.get('endDate', ''), 'closed': market.get('closed', False), } except Exception as e: logger.error(f"获取市场失败: {e}") return None def get_current_hourly_market(self) -> Optional[Dict]: """获取BTC市场""" slug = self.generate_market_slug() logger.info(f"搜索市场: {slug}") market = self.get_market_by_slug(slug) if market: self.current_market = market self.last_price_update = datetime.now() logger.info(f"✓ 找到市场: {market['title']}") logger.info(f" Up价格: ${market['outcome_prices'][0]:.3f}") logger.info(f" Down价格: ${market['outcome_prices'][1]:.3f}") else: logger.warning(f"❌ 未找到市场: {slug}") return market def refresh_prices(self) -> Optional[Dict]: """刷新当前市场价格""" if not self.current_market: self.get_current_hourly_market() # 使用CLOB API获取实时价格 tokens = self.current_market.get('tokens', []) if len(tokens) >= 2: realtime_prices = self.get_realtime_prices(tokens[0], tokens[1]) if realtime_prices: old_prices = self.current_market['outcome_prices'] new_prices = realtime_prices self.current_market['outcome_prices'] = new_prices self.last_price_update = datetime.now() # 显示价格变化 up_change = new_prices[0] - old_prices[0] down_change = new_prices[1] - old_prices[1] logger.info(f"📊 实时价格 [{datetime.now().strftime('%H:%M:%S')}]") logger.info(f" Up: ${new_prices[0]:.3f} ({up_change:+.3f})") logger.info(f" Down: ${new_prices[1]:.3f} ({down_change:+.3f})") return self.current_market # 备用: 使用Gamma API slug = self.current_market['slug'] market = self.get_market_by_slug(slug) if market: old_prices = self.current_market['outcome_prices'] new_prices = market['outcome_prices'] self.current_market = market self.last_price_update = datetime.now() # 显示价格变化 up_change = new_prices[0] - old_prices[0] down_change = new_prices[1] - old_prices[1] logger.info(f"📊 价格更新(Gamma) [{datetime.now().strftime('%H:%M:%S')}]") logger.info(f" Up: ${new_prices[0]:.3f} ({up_change:+.3f})") logger.info(f" Down: ${new_prices[1]:.3f} ({down_change:+.3f})") return market def get_realtime_prices(self, up_token_id: str, down_token_id: str) -> Optional[list]: """从CLOB API获取实时买入价格""" try: # 使用/price端点获取买入价格 up_response = self.http.get( f"{self.CLOB_API}/price", params={'token_id': up_token_id, 'side': 'BUY'} ) up_response.raise_for_status() up_data = up_response.json() down_response = self.http.get( f"{self.CLOB_API}/price", params={'token_id': down_token_id, 'side': 'BUY'} ) down_response.raise_for_status() down_data = down_response.json() # 解析买入价格 up_price = float(up_data.get('price', 0.5)) down_price = float(down_data.get('price', 0.5)) return [up_price, down_price] except Exception as e: logger.debug(f"获取实时价格失败: {e}") return None def execute_trade(self, prediction: int, market: Dict) -> Dict: """执行交易""" result = { 'timestamp': datetime.now().isoformat(), 'market': market['title'], 'prediction': 'UP' if prediction == 1 else 'DOWN', 'success': False, 'order': None, 'error': None, } if prediction == 1: # 预测涨 -> 买UP token_id = market['tokens'][0] price = market['outcome_prices'][0] side = "UP" else: # 预测跌 -> 买DOWN token_id = market['tokens'][1] price = market['outcome_prices'][1] side = "DOWN" result['side'] = side result['price'] = price # 检查价格限制 if price > self.max_price: result['error'] = f"价格${price:.2f}超过限制${self.max_price:.2f}" logger.error(f"❌ {result['error']}") return result # 计算数量 size = self.max_bet / price result['size'] = size result['amount'] = self.max_bet logger.info("📋 交易详情:") logger.info(f" 方向: {side}") logger.info(f" 价格: ${price:.3f}") logger.info(f" 金额: ${self.max_bet:.2f} USDC") logger.info(f" 数量: {size:.2f} 股") if self.dry_run: logger.info("🔸 [模拟模式] 未实际下单") result['success'] = True result['order'] = 'DRY_RUN' return result # 实际下单 if not self.clob_client: result['error'] = "CLOB客户端未初始化" logger.error(f"❌ {result['error']}") return result try: from py_clob_client.order_builder.constants import BUY order = self.clob_client.create_limit_order( token_id=token_id, price=price, size=size, side=BUY, ) response = self.clob_client.post_order(order) logger.info(f"✓ 订单已提交: {response}") result['success'] = True result['order'] = response except Exception as e: result['error'] = str(e) logger.error(f"❌ 下单失败: {e}") return result def get_prediction_with_delay(self, delay_seconds: float = 2.0) -> Tuple[int, float]: """ 延迟后获取K线并预测当前小时的涨跌 (多模型) """ logger.info(f"⏳ 等待{delay_seconds}秒获取最新K线...") time.sleep(delay_seconds) logger.info("获取最新BTC数据...") end_date = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d') start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') df = self.fetcher.fetch_ohlcv( symbol='BTC', timeframe='1h', start_date=start_date, end_date=end_date, use_cache=False # 不使用缓存,获取最新数据 ) # 使用倒数第二根K线(上一小时收盘)进行预测 prev_time = df['timestamp'].iloc[-2] prev_close = df['close'].iloc[-2] current_time = df['timestamp'].iloc[-1] current_close = df['close'].iloc[-1] logger.info(f" 上一K线时间: {prev_time} (用于预测)") logger.info(f" 上一收盘价: ${prev_close:,.2f}") logger.info(f" 当前K线时间: {current_time} (预测目标)") logger.info(f" 当前收盘价: ${current_close:,.2f} (未收盘)") # 回填上一小时的实际结果 (多模型) self._update_previous_prediction(df) # 用除最后一根外的数据进行预测 df_for_prediction = df.iloc[:-1] # 打印模型输入的最后一根K线数据 last_input_row = df_for_prediction.iloc[-1] logger.info(f" 📌 模型输入末根K线: {last_input_row['timestamp']} " f"O:{last_input_row['open']:.2f} H:{last_input_row['high']:.2f} " f"L:{last_input_row['low']:.2f} C:{last_input_row['close']:.2f} " f"V:{last_input_row['volume']:.0f}") # === 多模型预测 === all_preds = self.inference_manager.predict_all(df_for_prediction) # 解析预测结果 final_prediction = 0 final_probability = 0.5 # 1. DLinear if 'dlinear' in all_preds: dl = all_preds['dlinear'] dl_dir = "📈 UP" if dl['prediction'] == 1 else "📉 DOWN" logger.info(f" [DLinear] {dl_dir} (conf: {dl['probability']:.2%})") # 2. N-BEATS if 'nbeats' in all_preds: nb = all_preds['nbeats'] nb_dir = "📈 UP" if nb['prediction'] == 1 else "📉 DOWN" logger.info(f" [N-BEATS] {nb_dir}") # 3. TTM-R2 if 'ttm' in all_preds: ttm = all_preds['ttm'] ttm_dir = "📈 UP" if ttm['prediction'] == 1 else "📉 DOWN" logger.info(f" [TTM-R2] {ttm_dir} (conf: {ttm['probability']:.2%})") # 4. 集成结果 if 'ensemble' in all_preds and all_preds['ensemble']: ens = all_preds['ensemble'] ens_pred = ens['prediction'] votes = ens['votes'] ens_dir = "📈 UP" if ens_pred == 1 else "📉 DOWN" vote_str = f"{sum(votes)}/{len(votes)}" logger.info(f" [Ensemble] {ens_dir} (Votes: {vote_str})") # 使用集成结果作为最终决策 final_prediction = ens_pred final_probability = 0.6 # 集成策略暂时给一个固定置信度或基于投票比例 if len(votes) > 0: final_probability = sum(votes) / len(votes) if ens_pred == 1 else 1 - (sum(votes) / len(votes)) else: # 降级到 DLinear if 'dlinear' in all_preds: final_prediction = all_preds['dlinear']['prediction'] final_probability = all_preds['dlinear']['probability'] logger.warning("⚠️ 集成预测不可用,降级为 DLinear") # 保存所有预测结果到 hourly_model_predictions 表 (V2) try: et_tz = ZoneInfo('America/New_York') current_time_utc = pd.Timestamp(current_time) current_time_et = current_time_utc.tz_localize('UTC').tz_convert(et_tz) prediction_data = { "timestamp": current_time_utc.isoformat(), "timestamp_et": current_time_et.isoformat(), "date": current_time_et.strftime('%Y-%m-%d'), "hour": current_time_et.hour, "open_price": float(df['open'].iloc[-1]), "close_price": None, "actual_result": None, # DLinear "dlinear_prediction": all_preds.get('dlinear', {}).get('prediction'), "dlinear_prob": all_preds.get('dlinear', {}).get('probability'), "dlinear_correct": None, # N-BEATS "nbeats_prediction": all_preds.get('nbeats', {}).get('prediction'), "nbeats_prob": all_preds.get('nbeats', {}).get('probability'), "nbeats_correct": None, # TTM-R2 "ttm_prediction": all_preds.get('ttm', {}).get('prediction'), "ttm_prob": all_preds.get('ttm', {}).get('probability'), "ttm_correct": None, # Ensemble "ensemble_prediction": all_preds.get('ensemble', {}).get('prediction'), "ensemble_votes": all_preds.get('ensemble', {}).get('votes'), "ensemble_correct": None, } PredictionModel.save_hourly_prediction(prediction_data) logger.info(f"💾 所有模型预测记录已保存 (ET {current_time_et.strftime('%Y-%m-%d %H:%M')})") except Exception as e: logger.error(f"保存预测记录失败: {e}") return final_prediction, final_probability def _update_previous_prediction(self, df: pd.DataFrame): """回填上一小时预测的实际结果 (所有模型)""" if len(df) < 2: return try: et_tz = ZoneInfo('America/New_York') prev_row = df.iloc[-2] prev_utc = pd.Timestamp(prev_row['timestamp']) prev_et = prev_utc.tz_localize('UTC').tz_convert(et_tz) prev_date = prev_et.strftime('%Y-%m-%d') prev_hour = prev_et.hour prev_open = float(prev_row['open']) prev_close = float(prev_row['close']) actual_result = 1 if prev_close > prev_open else 0 # 使用 V2 更新方法 (宽表) updated = PredictionModel.update_actual_result_v2( date=prev_date, hour=prev_hour, actual_result=actual_result, close_price=prev_close ) if updated: logger.info(f"📝 回填上一小时结果: ET {prev_date} {prev_hour}:00, " f"实际{'涨' if actual_result == 1 else '跌'} (O:{prev_open:.0f} C:{prev_close:.0f})") except Exception as e: logger.error(f"回填上一小时结果失败: {e}") def run_once(self) -> Dict: """运行一次交易流程(不等待整点)""" logger.info("运行单次交易...") return self.run_at_hour_start(delay_seconds=0) def run_at_hour_start(self, delay_seconds: float = 2.0, timeout_minutes: int = 50) -> Dict: """ 整点时执行的交易流程 """ logger.info("=" * 60) logger.info(f"⏰ 整点交易触发 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") logger.info("=" * 60) logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}") logger.info(f" 最大下注: ${self.max_bet:.2f} USDC") logger.info(f" 最高价格: ${self.max_price:.2f}") logger.info(f" 等待超时: {timeout_minutes}分钟") result = { 'timestamp': datetime.now().isoformat(), 'success': False, 'error': None, } # 1. 延迟后获取预测 prediction, probability = self.get_prediction_with_delay(delay_seconds) result['prediction'] = 'UP' if prediction == 1 else 'DOWN' result['probability'] = probability if prediction == 1 else 1 - probability # 2. 获取初始市场 market = self.get_current_hourly_market() if not market: result['error'] = '未找到市场' logger.error(f"❌ {result['error']}") self._log_to_db(result, None) return result result['market'] = market['title'] result['market_slug'] = market['slug'] if market['closed']: result['error'] = '市场已关闭' logger.error(f"❌ {result['error']}") self._log_to_db(result, market) return result # 检查是否已交易过该市场 check_status = 'test' if self.dry_run else 'success' if self.db.check_existing_trade(market['slug'], status=check_status): result['error'] = '该市场已存在成功交易记录' logger.warning(f"⚠️ {result['error']}, 跳过交易") return result # 3. 循环检查价格并交易 start_time = datetime.now() end_time = start_time + timedelta(minutes=timeout_minutes) logger.info(f"🔄 开始价格监控 (预测: {'UP' if prediction == 1 else 'DOWN'})...") while datetime.now() < end_time: # 刷新价格 market = self.refresh_prices() if not market: logger.warning("无法获取市场价格,重试中...") time.sleep(10) continue # 检查价格 price_idx = 0 if prediction == 1 else 1 current_price = market['outcome_prices'][price_idx] logger.info(f" 当前价格: ${current_price:.3f} (目标: <= ${self.max_price:.2f})") if current_price <= self.max_price: # 价格符合,执行交易 logger.info("✅ 价格满足条件,正在下单...") trade_result = self.execute_trade(prediction, market) result.update(trade_result) # 记录数据库 self._log_to_db(result, market) logger.info("=" * 60) return result else: # 价格过高,等待 wait_time = 30 # 等待30秒 elapsed = int((datetime.now() - start_time).total_seconds()) logger.info(f"⏳ 价格过高,继续等待... (已耗时: {elapsed}s)") time.sleep(wait_time) # 超时 result['error'] = f'等待价格超时 ({timeout_minutes}分钟)' result['price'] = market['outcome_prices'][0] if prediction == 1 else market['outcome_prices'][1] result['side'] = 'UP' if prediction == 1 else 'DOWN' logger.error(f"❌ {result['error']}") self._log_to_db(result, market) logger.info("=" * 60) return result def _show_initial_prediction(self): """ 启动时显示当前预测并尝试执行交易 (不记录Monitor状态) """ logger.info("\n📊 启动时初始预测检查:") # 计算剩余分钟数 now = datetime.now() next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) remaining_minutes = int((next_hour - now).total_seconds() / 60) # 超时时间设为剩余分钟数 - 10,最少为 1 分钟 timeout_minutes = max(1, remaining_minutes - 10) logger.info(f" 距离下一整点还有 {remaining_minutes} 分钟,设置超时为 {timeout_minutes} 分钟") # 直接调用 run_at_hour_start,延迟为0,立即执行交易逻辑 self.run_at_hour_start(delay_seconds=0, timeout_minutes=timeout_minutes) def wait_for_next_hour(self) -> datetime: """等待到下一个整点""" now = datetime.now() next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) wait_seconds = (next_hour - now).total_seconds() logger.info(f"⏳ 等待下一个整点: {next_hour.strftime('%H:%M:%S')}") logger.info(f" 剩余等待: {int(wait_seconds)}秒") return next_hour def run_scheduler(self, interval_minutes: int = 5, delay_seconds: float = 2.0): """定时运行交易器""" logger.info("=" * 60) logger.info("🤖 BTC Hourly自动交易 (集成版) 已启动") logger.info("=" * 60) logger.info(f" 模式: {'🔸 模拟' if self.dry_run else '💰 真实交易'}") logger.info(f" 下单时机: 每个整点后延迟{delay_seconds}秒") logger.info(f" 价格刷新: 每{interval_minutes}分钟") logger.info(f" 最大下注: ${self.max_bet:.2f} USDC") logger.info(f" 最高价格: ${self.max_price:.2f}") logger.info(" 按 Ctrl+C 停止") logger.info("=" * 60) # 立即获取整点的市场 self.get_current_hourly_market() # 启动时进行一次预测(不下单,仅显示) self._show_initial_prediction() # 每小时整点后执行交易 def hourly_trade(): self.run_at_hour_start(delay_seconds=delay_seconds) schedule.every().hour.at(":00").do(lambda: time.sleep(delay_seconds) or hourly_trade()) try: while True: schedule.run_pending() time.sleep(1) except KeyboardInterrupt: logger.info("✓ 交易器已停止") def close(self): """关闭资源""" self.http.close() def main(): parser = argparse.ArgumentParser(description='BTC Hourly自动交易 - 基于DLinear、N-BEATS、TTM-R2集成模型') parser.add_argument('--dry-run', action='store_true', default=True, help='模拟模式(不实际下单)') parser.add_argument('--real', action='store_true', help='真实交易模式') parser.add_argument('--max-bet', type=float, default=1.0, help='最大下注金额(USDC)') parser.add_argument('--max-price', type=float, default=0.50, help='最高可接受价格') parser.add_argument('--once', action='store_true', help='只执行一次(不启动定时器)') parser.add_argument('--interval', type=int, default=1, help='价格刷新间隔(分钟)') parser.add_argument('--check-market', action='store_true', help='只检查当前市场信息') args = parser.parse_args() dry_run = not args.real # 真实交易确认 if not dry_run: logger.warning("⚠️ 警告: 即将进行真实交易!") logger.warning(f" 最大下注: ${args.max_bet:.2f} USDC") confirm = input("确认继续? (yes/no): ") if confirm.lower() != 'yes': logger.info("已取消") return trader = PolymarketBTCTrader( max_bet=args.max_bet, max_price=args.max_price, dry_run=dry_run, ) try: if args.check_market: # 只检查市场 trader.get_current_hourly_market() elif args.once: # 执行一次 result = trader.run_once() # 保存日志 log_file = 'trading_log.json' logs = [] if os.path.exists(log_file): with open(log_file, 'r') as f: try: logs = json.load(f) except: pass logs.append(result) with open(log_file, 'w') as f: json.dump(logs, f, indent=2, ensure_ascii=False) logger.info(f"结果已保存到 {log_file}") else: # 定时运行 trader.run_scheduler(interval_minutes=args.interval) finally: trader.close() if __name__ == "__main__": main()