351 lines
13 KiB
Python
Executable File
351 lines
13 KiB
Python
Executable File
#!/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()
|