244 lines
8.8 KiB
Python
244 lines
8.8 KiB
Python
"""
|
||
Signal Generation Scheduler - 定时生成交易信号
|
||
|
||
每隔指定时间间隔自动运行量化分析和LLM决策
|
||
"""
|
||
import asyncio
|
||
import logging
|
||
import signal
|
||
import sys
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
# Add parent directory to path
|
||
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
||
from config.settings import settings
|
||
from analysis.engine import MarketAnalysisEngine
|
||
from signals.quantitative import QuantitativeSignalGenerator
|
||
from signals.llm_decision import LLMDecisionMaker
|
||
from signals.llm_gate import LLMGate
|
||
from signals.aggregator import SignalAggregator
|
||
from notifiers.dingtalk import DingTalkNotifier
|
||
|
||
# Setup logging
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class SignalScheduler:
|
||
"""定时信号生成调度器"""
|
||
|
||
def __init__(self, interval_minutes: int = 5):
|
||
"""
|
||
Args:
|
||
interval_minutes: 生成信号的时间间隔(分钟)
|
||
"""
|
||
self.interval_minutes = interval_minutes
|
||
self.is_running = False
|
||
|
||
# Initialize components
|
||
self.engine = MarketAnalysisEngine()
|
||
self.quant_generator = QuantitativeSignalGenerator()
|
||
|
||
# Initialize LLM gate
|
||
self.llm_gate = None
|
||
if settings.LLM_GATE_ENABLED:
|
||
self.llm_gate = LLMGate(
|
||
min_candles=settings.LLM_MIN_CANDLES,
|
||
min_composite_score=settings.LLM_MIN_COMPOSITE_SCORE,
|
||
max_calls_per_day=settings.LLM_MAX_CALLS_PER_DAY,
|
||
min_call_interval_minutes=settings.LLM_MIN_INTERVAL_MINUTES,
|
||
)
|
||
|
||
# Initialize LLM decision maker
|
||
self.llm_maker = LLMDecisionMaker(provider='openai')
|
||
|
||
# Initialize DingTalk notifier
|
||
import os
|
||
dingtalk_webhook = os.getenv('DINGTALK_WEBHOOK')
|
||
dingtalk_secret = os.getenv('DINGTALK_SECRET')
|
||
self.dingtalk = DingTalkNotifier(
|
||
webhook_url=dingtalk_webhook,
|
||
secret=dingtalk_secret,
|
||
enabled=bool(dingtalk_webhook)
|
||
)
|
||
|
||
logger.info(f"🤖 Signal Scheduler 初始化完成 - 每{interval_minutes}分钟生成一次信号")
|
||
|
||
async def generate_signal_once(self) -> dict:
|
||
"""执行一次信号生成"""
|
||
try:
|
||
logger.info("=" * 80)
|
||
logger.info(f"📊 开始生成交易信号 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
logger.info("=" * 80)
|
||
|
||
# Step 1: Market analysis
|
||
analysis = self.engine.analyze_current_market(timeframe='5m')
|
||
|
||
if 'error' in analysis:
|
||
logger.warning(f"⚠️ 市场分析失败: {analysis['error']}")
|
||
return None
|
||
|
||
logger.info(f"✅ 市场分析完成 - 价格: ${analysis['current_price']:,.2f}, 趋势: {analysis['trend_analysis'].get('direction')}")
|
||
|
||
# Step 2: Quantitative signal
|
||
quant_signal = self.quant_generator.generate_signal(analysis)
|
||
logger.info(f"📈 量化信号: {quant_signal['signal_type']} (得分: {quant_signal['composite_score']:.1f})")
|
||
|
||
# Step 3: Check LLM gate and generate LLM decision
|
||
llm_signal = None
|
||
should_call_llm = True
|
||
|
||
if self.llm_gate:
|
||
should_call_llm, gate_reason = self.llm_gate.should_call_llm(quant_signal, analysis)
|
||
|
||
if should_call_llm:
|
||
logger.info(f"✅ LLM Gate: PASSED - {gate_reason}")
|
||
else:
|
||
logger.info(f"🚫 LLM Gate: BLOCKED - {gate_reason}")
|
||
|
||
# Call LLM if gate passed
|
||
if should_call_llm:
|
||
llm_context = self.engine.get_llm_context(format='full')
|
||
llm_signal = self.llm_maker.generate_decision(llm_context, analysis)
|
||
|
||
if llm_signal.get('enabled', True):
|
||
logger.info(f"🤖 LLM信号: {llm_signal['signal_type']} (置信度: {llm_signal.get('confidence', 0):.2%})")
|
||
else:
|
||
logger.info("⚠️ LLM未启用 (无API key)")
|
||
|
||
# Step 4: Aggregate signals
|
||
aggregated = SignalAggregator.aggregate_signals(quant_signal, llm_signal)
|
||
|
||
logger.info(f"🎯 最终信号: {aggregated['final_signal']} (置信度: {aggregated['final_confidence']:.2%})")
|
||
|
||
# Step 5: Save to file
|
||
output_file = Path(__file__).parent / 'output' / 'latest_signal.json'
|
||
output_file.parent.mkdir(exist_ok=True)
|
||
|
||
import json
|
||
output_data = {
|
||
'timestamp': datetime.now().isoformat(),
|
||
'aggregated_signal': aggregated,
|
||
'market_analysis': {
|
||
'price': analysis['current_price'],
|
||
'trend': analysis['trend_analysis'],
|
||
'momentum': analysis['momentum'],
|
||
},
|
||
'quantitative_signal': quant_signal,
|
||
'llm_signal': llm_signal if llm_signal and llm_signal.get('enabled', True) else None,
|
||
}
|
||
|
||
with open(output_file, 'w') as f:
|
||
json.dump(output_data, f, indent=2, ensure_ascii=False)
|
||
|
||
logger.info(f"💾 信号已保存到: {output_file}")
|
||
|
||
# Step 6: Send DingTalk notification
|
||
try:
|
||
# 发送通知的条件:
|
||
# 1. BUY/SELL 明确信号
|
||
# 2. HOLD信号但有日内交易机会
|
||
final_signal = aggregated.get('final_signal', 'HOLD')
|
||
|
||
should_notify = False
|
||
notify_reason = ""
|
||
|
||
if final_signal in ['BUY', 'SELL']:
|
||
should_notify = True
|
||
notify_reason = f"明确{final_signal}信号"
|
||
elif final_signal == 'HOLD':
|
||
# 检查是否有日内机会
|
||
llm_signal = aggregated.get('llm_signal')
|
||
if llm_signal and isinstance(llm_signal, dict):
|
||
opportunities = llm_signal.get('opportunities', {})
|
||
short_term = opportunities.get('short_term_5m_15m_1h', {})
|
||
if short_term.get('exists', False):
|
||
should_notify = True
|
||
direction = short_term.get('direction', 'N/A')
|
||
notify_reason = f"HOLD信号,但存在短期{direction}机会"
|
||
|
||
if should_notify:
|
||
logger.info(f"📱 发送钉钉通知 - {notify_reason}")
|
||
sent = self.dingtalk.send_signal(aggregated)
|
||
if sent:
|
||
logger.info(f"✅ 钉钉通知发送成功")
|
||
else:
|
||
logger.warning(f"⚠️ 钉钉通知发送失败或未配置")
|
||
else:
|
||
logger.info(f"ℹ️ HOLD信号且无日内机会,跳过钉钉通知")
|
||
except Exception as e:
|
||
logger.error(f"❌ 钉钉通知发送异常: {e}", exc_info=True)
|
||
|
||
logger.info("=" * 80)
|
||
|
||
return aggregated
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 信号生成失败: {e}", exc_info=True)
|
||
return None
|
||
|
||
async def run(self):
|
||
"""启动调度器主循环"""
|
||
self.is_running = True
|
||
logger.info(f"🚀 Signal Scheduler 启动 - 每{self.interval_minutes}分钟生成信号")
|
||
|
||
# 立即生成一次
|
||
await self.generate_signal_once()
|
||
|
||
# 定时循环
|
||
while self.is_running:
|
||
try:
|
||
# 等待指定时间间隔
|
||
await asyncio.sleep(self.interval_minutes * 60)
|
||
|
||
# 生成信号
|
||
await self.generate_signal_once()
|
||
|
||
except asyncio.CancelledError:
|
||
logger.info("调度器收到取消信号")
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"调度器错误: {e}", exc_info=True)
|
||
await asyncio.sleep(60) # 错误后等待1分钟再继续
|
||
|
||
logger.info("🛑 Signal Scheduler 已停止")
|
||
|
||
def stop(self):
|
||
"""停止调度器"""
|
||
self.is_running = False
|
||
|
||
|
||
async def main():
|
||
"""主入口"""
|
||
# 从环境变量或默认值获取间隔
|
||
import os
|
||
interval = int(os.getenv('SIGNAL_INTERVAL_MINUTES', '5'))
|
||
|
||
scheduler = SignalScheduler(interval_minutes=interval)
|
||
|
||
# Setup signal handlers for graceful shutdown
|
||
def signal_handler(sig, _frame):
|
||
logger.info(f"收到信号 {sig},正在关闭...")
|
||
scheduler.stop()
|
||
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
signal.signal(signal.SIGTERM, signal_handler)
|
||
|
||
# Start scheduler
|
||
await scheduler.run()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
asyncio.run(main())
|
||
except KeyboardInterrupt:
|
||
logger.info("用户中断")
|
||
except Exception as e:
|
||
logger.error(f"致命错误: {e}", exc_info=True)
|
||
sys.exit(1)
|