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:
mindesbunister
2025-11-07 20:40:07 +01:00
parent 6d5991172a
commit 9b767342dc
14 changed files with 1150 additions and 568 deletions

View File

@@ -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}"