feat: Implement re-entry analytics system with fresh TradingView data
- Add market data cache service (5min expiry) for storing TradingView metrics - Create /api/trading/market-data webhook endpoint for continuous data updates - Add /api/analytics/reentry-check endpoint for validating manual trades - Update execute endpoint to auto-cache metrics from incoming signals - Enhance Telegram bot with pre-execution analytics validation - Support --force flag to override analytics blocks - Use fresh ADX/ATR/RSI data when available, fallback to historical - Apply performance modifiers: -20 for losing streaks, +10 for winning - Minimum re-entry score 55 (vs 60 for new signals) - Fail-open design: proceeds if analytics unavailable - Show data freshness and source in Telegram responses - Add comprehensive setup guide in docs/guides/REENTRY_ANALYTICS_QUICKSTART.md Phase 1 implementation for smart manual trade validation.
This commit is contained in:
@@ -530,7 +530,7 @@ async def trade_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
||||
|
||||
async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Execute manual long/short commands sent as plain text."""
|
||||
"""Execute manual long/short commands sent as plain text with analytics validation."""
|
||||
|
||||
if update.message is None:
|
||||
return
|
||||
@@ -541,6 +541,12 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
|
||||
text = update.message.text.strip().lower()
|
||||
|
||||
parts = text.split()
|
||||
|
||||
# Check for --force flag
|
||||
force_trade = '--force' in parts
|
||||
if force_trade:
|
||||
parts.remove('--force')
|
||||
|
||||
if len(parts) != 2:
|
||||
return
|
||||
|
||||
@@ -553,6 +559,76 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
|
||||
if not symbol_info:
|
||||
return
|
||||
|
||||
# Convert to Drift format for analytics check
|
||||
drift_symbol_map = {
|
||||
'sol': 'SOL-PERP',
|
||||
'eth': 'ETH-PERP',
|
||||
'btc': 'BTC-PERP'
|
||||
}
|
||||
drift_symbol = drift_symbol_map.get(symbol_key)
|
||||
|
||||
# 🆕 PHASE 1: Check analytics before executing (unless forced)
|
||||
if not force_trade:
|
||||
try:
|
||||
print(f"🔍 Checking re-entry analytics for {direction.upper()} {drift_symbol}", flush=True)
|
||||
|
||||
analytics_response = requests.post(
|
||||
f"{TRADING_BOT_URL}/api/analytics/reentry-check",
|
||||
json={'symbol': drift_symbol, 'direction': direction},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if analytics_response.ok:
|
||||
analytics = analytics_response.json()
|
||||
|
||||
if not analytics.get('should_enter'):
|
||||
# Build rejection message with data source info
|
||||
data_source = analytics.get('data_source', 'unknown')
|
||||
data_age = analytics.get('data_age_seconds')
|
||||
|
||||
data_emoji = {
|
||||
'tradingview_real': '✅',
|
||||
'fallback_historical': '⚠️',
|
||||
'no_data': '❌'
|
||||
}
|
||||
data_icon = data_emoji.get(data_source, '❓')
|
||||
|
||||
data_age_text = f" ({data_age}s old)" if data_age else ""
|
||||
|
||||
message = (
|
||||
f"🛑 *Analytics suggest NOT entering {direction.upper()} {symbol_info['label']}*\n\n"
|
||||
f"*Reason:* {analytics.get('reason', 'Unknown')}\n"
|
||||
f"*Score:* {analytics.get('score', 0)}/100\n"
|
||||
f"*Data:* {data_icon} {data_source}{data_age_text}\n\n"
|
||||
f"Use `{text} --force` to override"
|
||||
)
|
||||
|
||||
await update.message.reply_text(message, parse_mode='Markdown')
|
||||
print(f"❌ Trade blocked by analytics (score: {analytics.get('score')})", flush=True)
|
||||
return
|
||||
|
||||
# Analytics passed - show confirmation
|
||||
data_age = analytics.get('data_age_seconds')
|
||||
data_source = analytics.get('data_source', 'unknown')
|
||||
data_age_text = f" ({data_age}s old)" if data_age else ""
|
||||
|
||||
confirm_message = (
|
||||
f"✅ *Analytics check passed ({analytics.get('score')}/100)*\n"
|
||||
f"Data: {data_source}{data_age_text}\n"
|
||||
f"Proceeding with {direction.upper()} {symbol_info['label']}..."
|
||||
)
|
||||
|
||||
await update.message.reply_text(confirm_message, parse_mode='Markdown')
|
||||
print(f"✅ Analytics passed (score: {analytics.get('score')})", flush=True)
|
||||
else:
|
||||
# Analytics endpoint failed - proceed with trade (fail-open)
|
||||
print(f"⚠️ Analytics check failed ({analytics_response.status_code}) - proceeding anyway", flush=True)
|
||||
|
||||
except Exception as analytics_error:
|
||||
# Analytics check error - proceed with trade (fail-open)
|
||||
print(f"⚠️ Analytics error: {analytics_error} - proceeding anyway", flush=True)
|
||||
|
||||
# Execute the trade
|
||||
metrics = MANUAL_METRICS[direction]
|
||||
|
||||
payload = {
|
||||
@@ -568,7 +644,7 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
|
||||
}
|
||||
|
||||
try:
|
||||
print(f"🚀 Manual trade: {direction.upper()} {symbol_info['label']}", flush=True)
|
||||
print(f"🚀 Manual trade: {direction.upper()} {symbol_info['label']}{' (FORCED)' if force_trade else ''}", flush=True)
|
||||
|
||||
response = requests.post(
|
||||
f"{TRADING_BOT_URL}/api/trading/execute",
|
||||
@@ -609,8 +685,10 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
|
||||
tp2_text = f"${tp2:.4f}" if tp2 is not None else 'n/a'
|
||||
sl_text = f"${sl:.4f}" if sl is not None else 'n/a'
|
||||
|
||||
force_indicator = " (FORCED)" if force_trade else ""
|
||||
|
||||
success_message = (
|
||||
f"✅ OPENED {direction.upper()} {symbol_info['label']}\n"
|
||||
f"✅ OPENED {direction.upper()} {symbol_info['label']}{force_indicator}\n"
|
||||
f"Entry: {entry_text}\n"
|
||||
f"Size: {size_text}\n"
|
||||
f"TP1: {tp1_text}\nTP2: {tp2_text}\nSL: {sl_text}"
|
||||
|
||||
Reference in New Issue
Block a user