tradusai/scripts/generate_trading_signal.py
2025-12-02 22:54:03 +08:00

351 lines
13 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Generate Trading Signal - Combine quantitative analysis and LLM decision making
"""
import sys
import json
import logging
import os
import argparse
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.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__)
def print_section(title: str, width: int = 80):
"""Print section header"""
print(f"\n{'=' * width}")
print(f"{title:^{width}}")
print(f"{'=' * width}")
def print_signal(signal: dict, title: str):
"""Pretty print a signal"""
print(f"\n{title}")
print("-" * 60)
print(f"Signal: {signal['signal_type']}")
print(f"Confidence: {signal.get('confidence', 0):.2%}")
# Display trade type if available (from LLM)
if 'trade_type' in signal:
trade_type = signal['trade_type']
trade_type_display = {
'INTRADAY': '📊 日内交易',
'SWING': '📈 中长线交易',
'NONE': '⏸️ 观望'
}.get(trade_type, trade_type)
print(f"Trade Type: {trade_type_display}")
if 'composite_score' in signal:
print(f"Composite Score: {signal['composite_score']:.1f}")
if 'scores' in signal:
print("\nComponent Scores:")
for component, score in signal['scores'].items():
print(f" {component:12}: {score:>6.1f}")
if 'levels' in signal:
levels = signal['levels']
print(f"\nPrice Levels:")
print(f" Current: ${levels.get('current_price', 0):>10,.2f}")
print(f" Entry: ${levels.get('entry', 0):>10,.2f}")
print(f" Stop: ${levels.get('stop_loss', 0):>10,.2f}")
print(f" Target 1: ${levels.get('take_profit_1', 0):>10,.2f}")
print(f" Target 2: ${levels.get('take_profit_2', 0):>10,.2f}")
print(f" Target 3: ${levels.get('take_profit_3', 0):>10,.2f}")
if 'risk_reward_ratio' in signal:
rr = signal['risk_reward_ratio']
if rr > 0:
print(f"\nRisk/Reward: 1:{rr:.2f}")
# Display opportunities breakdown (from LLM)
if 'opportunities' in signal:
opps = signal['opportunities']
# Intraday opportunity
if opps.get('intraday', {}).get('exists'):
intra = opps['intraday']
print(f"\n📊 日内交易机会:")
print(f" 方向: {intra.get('direction', 'N/A')}")
if intra.get('entry_price'):
print(f" 入场: ${intra['entry_price']:,.2f}")
if intra.get('stop_loss'):
print(f" 止损: ${intra['stop_loss']:,.2f}")
if intra.get('take_profit'):
print(f" 止盈: ${intra['take_profit']:,.2f}")
if intra.get('reasoning'):
print(f" 说明: {intra['reasoning']}")
# Swing opportunity
if opps.get('swing', {}).get('exists'):
swing = opps['swing']
print(f"\n📈 中长线交易机会:")
print(f" 方向: {swing.get('direction', 'N/A')}")
if swing.get('entry_price'):
print(f" 入场: ${swing['entry_price']:,.2f}")
if swing.get('stop_loss'):
print(f" 止损: ${swing['stop_loss']:,.2f}")
if swing.get('take_profit'):
print(f" 止盈: ${swing['take_profit']:,.2f}")
if swing.get('reasoning'):
print(f" 说明: {swing['reasoning']}")
# Ambush opportunity
if opps.get('ambush', {}).get('exists'):
ambush = opps['ambush']
print(f"\n📌 埋伏点位:")
if ambush.get('price_level'):
print(f" 埋伏价位: ${ambush['price_level']:,.2f}")
if ambush.get('reasoning'):
print(f" 说明: {ambush['reasoning']}")
if 'reasoning' in signal:
print(f"\nReasoning: {signal['reasoning']}")
def print_aggregated_signal(aggregated: dict):
"""Print aggregated signal"""
print_section("📊 AGGREGATED TRADING SIGNAL")
print(f"\n🎯 Final Signal: {aggregated['final_signal']}")
print(f"📈 Confidence: {aggregated['final_confidence']:.2%}")
print(f"🤝 Consensus: {aggregated['consensus']}")
print(f"✅ Agreement Score: {aggregated['agreement_score']:.2%}")
# Quantitative signal
print("\n" + "" * 80)
quant = aggregated['quantitative_signal']
print(f"🔢 QUANTITATIVE SIGNAL: {quant.get('signal_type', quant.get('signal', 'HOLD'))} (confidence: {quant.get('confidence', 0):.2%})")
print(f" Composite Score: {quant.get('composite_score', 0):.1f}")
if 'scores' in quant:
scores = quant['scores']
print(f" Trend: {scores.get('trend', 0):>6.1f} | "
f"Momentum: {scores.get('momentum', 0):>6.1f} | "
f"OrderFlow: {scores.get('orderflow', 0):>6.1f} | "
f"Breakout: {scores.get('breakout', 0):>6.1f}")
# LLM signal
print("\n" + "" * 80)
llm = aggregated.get('llm_signal')
if llm and isinstance(llm, dict):
trade_type_icon = {
'INTRADAY': '📊',
'SWING': '📈',
'AMBUSH': '📌',
'NONE': '⏸️'
}.get(llm.get('trade_type', 'NONE'), '')
trade_type_text = {
'INTRADAY': '日内交易',
'SWING': '中长线',
'AMBUSH': '埋伏',
'NONE': '观望'
}.get(llm.get('trade_type', 'NONE'), llm.get('trade_type', 'N/A'))
print(f"🤖 LLM SIGNAL: {llm.get('signal_type', llm.get('signal', 'HOLD'))} (confidence: {llm.get('confidence', 0):.2%})")
print(f" Trade Type: {trade_type_icon} {trade_type_text}")
# Display opportunities if available
if 'opportunities' in llm:
opps = llm['opportunities']
if opps.get('intraday', {}).get('exists'):
intra = opps['intraday']
print(f" 📊 日内: {intra.get('direction')} @ ${intra.get('entry_price', 0):,.0f}")
if opps.get('swing', {}).get('exists'):
swing = opps['swing']
print(f" 📈 中长线: {swing.get('direction')} @ ${swing.get('entry_price', 0):,.0f}")
if opps.get('ambush', {}).get('exists'):
ambush = opps['ambush']
print(f" 📌 埋伏: ${ambush.get('price_level', 0):,.0f}")
print(f" Reasoning: {llm.get('reasoning', 'N/A')[:200]}")
if llm.get('key_factors'):
print(f" Key Factors: {', '.join(llm['key_factors'][:3])}")
else:
print("🤖 LLM SIGNAL: Not available (no API key configured)")
# Final levels
print("\n" + "" * 80)
levels = aggregated['levels']
print("💰 RECOMMENDED LEVELS:")
print(f" Current Price: ${levels['current_price']:>10,.2f}")
print(f" Entry: ${levels['entry']:>10,.2f}")
print(f" Stop Loss: ${levels['stop_loss']:>10,.2f}")
print(f" Take Profit 1: ${levels['take_profit_1']:>10,.2f}")
print(f" Take Profit 2: ${levels['take_profit_2']:>10,.2f}")
print(f" Take Profit 3: ${levels['take_profit_3']:>10,.2f}")
rr = aggregated.get('risk_reward_ratio', 0)
if rr > 0:
print(f"\n Risk/Reward Ratio: 1:{rr:.2f}")
# Recommendation
print("\n" + "" * 80)
print(f"💡 RECOMMENDATION:")
print(f" {aggregated['recommendation']}")
# Warnings
if aggregated.get('warnings'):
print("\n" + "" * 80)
print("⚠️ WARNINGS:")
for warning in aggregated['warnings']:
print(f" {warning}")
print("\n" + "=" * 80)
def main():
# Parse command line arguments
parser = argparse.ArgumentParser(description='Generate trading signals')
parser.add_argument('--send-dingtalk', action='store_true',
help='Send notification to DingTalk')
args = parser.parse_args()
print_section("🚀 TRADING SIGNAL GENERATOR", 80)
# Initialize components
logger.info("Initializing analysis engine...")
engine = MarketAnalysisEngine()
logger.info("Initializing signal generators...")
quant_generator = QuantitativeSignalGenerator()
# Initialize DingTalk notifier if requested
dingtalk = None
if args.send_dingtalk:
dingtalk_webhook = os.getenv('DINGTALK_WEBHOOK')
dingtalk_secret = os.getenv('DINGTALK_SECRET')
dingtalk = DingTalkNotifier(
webhook_url=dingtalk_webhook,
secret=dingtalk_secret,
enabled=bool(dingtalk_webhook)
)
# Initialize LLM gate (极简门控 - 频率为主,量化初筛)
llm_gate = None
if settings.LLM_GATE_ENABLED:
logger.info("Initializing simplified LLM gate...")
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,
)
# Try to initialize LLM (will be disabled if no API key)
# Use 'openai' provider - supports OpenAI, Deepseek, and other OpenAI-compatible APIs
llm_maker = LLMDecisionMaker(provider='openai') # or 'claude'
# Step 1: Perform market analysis
print_section("1⃣ MARKET ANALYSIS")
analysis = engine.analyze_current_market(timeframe='5m')
if 'error' in analysis:
print(f"❌ Error: {analysis['error']}")
print("\n💡 Tip: Wait for more data to accumulate (need at least 200 candles)")
return
print(f"✅ Analysis complete")
print(f" Price: ${analysis['current_price']:,.2f}")
print(f" Trend: {analysis['trend_analysis'].get('direction', 'unknown')}")
print(f" RSI: {analysis['momentum'].get('rsi', 0):.1f}")
print(f" MACD: {analysis['momentum'].get('macd_signal', 'unknown')}")
# Step 2: Generate quantitative signal
print_section("2⃣ QUANTITATIVE SIGNAL")
quant_signal = quant_generator.generate_signal(analysis)
print_signal(quant_signal, "📊 Quantitative Analysis")
# Step 3: Check LLM gate and generate LLM decision
print_section("3⃣ LLM DECISION")
llm_signal = None
should_call_llm = True
gate_reason = "LLM gate disabled"
# Check LLM gate prerequisites
if llm_gate:
should_call_llm, gate_reason = llm_gate.should_call_llm(quant_signal, analysis)
if should_call_llm:
print(f"\n✅ LLM Gate: PASSED")
print(f" Reason: {gate_reason}")
else:
print(f"\n❌ LLM Gate: BLOCKED")
print(f" Reason: {gate_reason}")
print(f"\n💡 LLM will NOT be called. Using quantitative signal only.")
print(f" Quantitative score: {quant_signal.get('composite_score', 0):.1f}")
print(f" Quantitative confidence: {quant_signal.get('confidence', 0):.2%}")
# Call LLM only if gate passed
if should_call_llm:
llm_context = engine.get_llm_context(format='full')
llm_signal = llm_maker.generate_decision(llm_context, analysis)
if llm_signal.get('enabled', True):
print_signal(llm_signal, "🤖 LLM Analysis")
else:
print("\n🤖 LLM Analysis: Disabled (no API key)")
print(" Set ANTHROPIC_API_KEY or OPENAI_API_KEY to enable")
else:
# LLM blocked by gate, use None (aggregator will use quant-only)
print("\n🤖 LLM Analysis: Skipped (gate blocked)")
# Step 4: Aggregate signals
print_section("4⃣ SIGNAL AGGREGATION")
aggregated = SignalAggregator.aggregate_signals(quant_signal, llm_signal)
print_aggregated_signal(aggregated)
# Step 5: Export to JSON
output_file = Path(__file__).parent.parent / 'output' / 'latest_signal.json'
output_file.parent.mkdir(exist_ok=True)
output_data = {
'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)
print(f"\n💾 Signal saved to: {output_file}")
# Send DingTalk notification if enabled
if dingtalk:
print(f"\n📱 Sending DingTalk notification...")
success = dingtalk.send_signal(aggregated)
if success:
print(f"✅ DingTalk notification sent successfully")
else:
print(f"❌ Failed to send DingTalk notification")
print_section("✅ SIGNAL GENERATION COMPLETE", 80)
if __name__ == "__main__":
main()