Fix P&L calculation and signal flip detection

- Fix external closure P&L using tp1Hit flag instead of currentSize
- Add direction change detection to prevent false TP1 on signal flips
- Signal flips now recorded with accurate P&L as 'manual' exits
- Add retry logic with exponential backoff for Solana RPC rate limits
- Create /api/trading/cancel-orders endpoint for manual cleanup
- Improves data integrity for win/loss statistics
This commit is contained in:
mindesbunister
2025-11-09 17:59:50 +01:00
parent 4d533ccb53
commit 22195ed34c
15 changed files with 2166 additions and 17 deletions

View File

@@ -0,0 +1,368 @@
# Analytics System Status & Next Steps
**Date:** November 8, 2025
## 📊 Current Status
### ✅ What's Already Working
**1. Re-Entry Analytics System (Phase 1) - IMPLEMENTED**
- ✅ Market data cache service (`lib/trading/market-data-cache.ts`)
-`/api/trading/market-data` webhook endpoint (GET/POST)
-`/api/analytics/reentry-check` validation endpoint
- ✅ Telegram bot integration with analytics pre-check
- ✅ Auto-caching of metrics from TradingView signals
-`--force` flag override capability
**2. Data Collection - IN PROGRESS**
- ✅ 122 total completed trades
- ✅ 59 trades with signal quality scores (48%)
- ✅ 67 trades with MAE/MFE data (55%)
- ✅ Good data split: 32 shorts (avg score 73.9), 27 longs (avg score 70.4)
**3. Code Infrastructure - READY**
- ✅ Signal quality scoring system with timeframe awareness
- ✅ MAE/MFE tracking in Position Manager
- ✅ Database schema with all necessary fields
- ✅ Analytics endpoints ready for expansion
### ⚠️ What's NOT Yet Configured
**1. TradingView Market Data Alerts - MISSING**
- No alerts firing every 1-5 minutes to update cache
- This is why market data cache is empty: `{"availableSymbols":[],"count":0,"cache":{}}`
- **CRITICAL:** Without this, manual Telegram trades use stale/historical data
**2. Optimal SL/TP Analytics - NOT IMPLEMENTED**
- Have 59 trades with quality scores (need 70-100 for Phase 2)
- Have MAE/MFE data showing:
- Shorts: Avg MFE +3.63%, MAE -4.52%
- Longs: Avg MFE +4.01%, MAE -2.59%
- Need SQL analysis to determine optimal exit levels
- Need to implement ATR-based dynamic targets
**3. Entry Quality Analytics - PARTIALLY IMPLEMENTED** ⚙️
- Signal quality scoring: ✅ Working
- Re-entry validation: ✅ Working (but no fresh data)
- Performance-based modifiers: ✅ Working
- **Missing:** Fresh TradingView data due to missing alerts
---
## 🎯 Immediate Action Plan
### Priority 1: Setup TradingView Market Data Alerts (30 mins)
**This will enable fresh data for manual Telegram trades!**
#### For Each Symbol (SOL, ETH, BTC):
**Step 1:** Open TradingView chart
- Symbol: SOLUSDT (or ETHUSDT, BTCUSDT)
- Timeframe: 5-minute chart
**Step 2:** Create Alert
- Click Alert icon (🔔)
- Condition: `ta.change(time("1"))` (fires every bar close)
- Alert Name: `Market Data - SOL 5min`
**Step 3:** Webhook Configuration
- **URL:** `https://YOUR-DOMAIN.COM/api/trading/market-data`
- Example: `https://flow.egonetix.de/api/trading/market-data` (if bot is on same domain)
- Or: `http://YOUR-SERVER-IP:3001/api/trading/market-data` (if direct access)
**Step 4:** Alert Message (JSON)
```json
{
"action": "market_data",
"symbol": "{{ticker}}",
"timeframe": "{{interval}}",
"atr": {{ta.atr(14)}},
"adx": {{ta.dmi(14, 14)}},
"rsi": {{ta.rsi(14)}},
"volumeRatio": {{volume / ta.sma(volume, 20)}},
"pricePosition": {{(close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100}},
"currentPrice": {{close}}
}
```
**Step 5:** Settings
- Frequency: **Once Per Bar Close** (fires every 5 minutes)
- Expires: Never
- Send Webhook: ✅ Enabled
**Step 6:** Verify
```bash
# Wait 5 minutes, then check cache
curl http://localhost:3001/api/trading/market-data
# Should see:
# {"success":true,"availableSymbols":["SOL-PERP"],"count":1,"cache":{...}}
```
**Step 7:** Test Telegram
```
You: "long sol"
# Should now show:
# ✅ Data: tradingview_real (23s old) ← Fresh data!
```
---
### Priority 2: Run SQL Analysis for Optimal SL/TP (1 hour)
**Goal:** Determine data-driven optimal exit levels
#### Analysis Queries to Run:
**1. MFE/MAE Distribution Analysis**
```sql
-- See where trades actually move (not where we exit)
SELECT
direction,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_best_profit,
ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as q25_mfe,
ROUND(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as median_mfe,
ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as q75_mfe,
ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_worst_loss,
ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "maxAdverseExcursion")::numeric, 2) as q25_mae
FROM "Trade"
WHERE "exitReason" IS NOT NULL AND "maxFavorableExcursion" IS NOT NULL
GROUP BY direction;
```
**2. Quality Score vs Exit Performance**
```sql
-- Do high quality signals really move further?
SELECT
CASE
WHEN "signalQualityScore" >= 80 THEN 'High (80-100)'
WHEN "signalQualityScore" >= 70 THEN 'Medium (70-79)'
ELSE 'Low (60-69)'
END as quality_tier,
COUNT(*) as trades,
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe,
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate,
-- How many went beyond current TP2 (+0.7%)?
ROUND(100.0 * SUM(CASE WHEN "maxFavorableExcursion" > 0.7 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as pct_exceeded_tp2
FROM "Trade"
WHERE "signalQualityScore" IS NOT NULL AND "exitReason" IS NOT NULL
GROUP BY quality_tier
ORDER BY quality_tier;
```
**3. Runner Potential Analysis**
```sql
-- How often do trades move 2%+ (runner territory)?
SELECT
direction,
"exitReason",
COUNT(*) as count,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe,
SUM(CASE WHEN "maxFavorableExcursion" > 2.0 THEN 1 ELSE 0 END) as moved_beyond_2pct,
SUM(CASE WHEN "maxFavorableExcursion" > 3.0 THEN 1 ELSE 0 END) as moved_beyond_3pct,
SUM(CASE WHEN "maxFavorableExcursion" > 5.0 THEN 1 ELSE 0 END) as moved_beyond_5pct
FROM "Trade"
WHERE "exitReason" IS NOT NULL AND "maxFavorableExcursion" IS NOT NULL
GROUP BY direction, "exitReason"
ORDER BY direction, count DESC;
```
**4. ATR Correlation**
```sql
-- Does higher ATR = bigger moves?
SELECT
CASE
WHEN atr < 0.3 THEN 'Low (<0.3%)'
WHEN atr < 0.6 THEN 'Medium (0.3-0.6%)'
ELSE 'High (>0.6%)'
END as atr_bucket,
COUNT(*) as trades,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe,
ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae,
ROUND(AVG(atr)::numeric, 3) as avg_atr
FROM "Trade"
WHERE atr IS NOT NULL AND "exitReason" IS NOT NULL
GROUP BY atr_bucket
ORDER BY avg_atr;
```
#### Expected Insights:
After running these queries, you'll know:
-**Where to set TP1/TP2:** Based on median MFE (not averages, which are skewed by outliers)
-**Runner viability:** What % of trades actually move 3%+ (current runner territory)
-**Quality-based strategy:** Should high-score signals use different exits?
-**ATR effectiveness:** Does ATR predict movement range?
---
### Priority 3: Implement Optimal Exit Strategy (2-3 hours)
**ONLY AFTER** Priority 2 analysis shows clear improvements!
#### Based on preliminary data (shorts: +3.63% MFE, longs: +4.01% MFE):
**Option A: Conservative (Take What Market Gives)**
```typescript
// If median MFE is around 2-3%, don't chase runners
TP1: +0.4% Close 75% (current)
TP2: +0.7% Close 25% (no runner)
SL: -1.5% (current)
```
**Option B: Runner-Friendly (If >50% trades exceed +2%)**
```typescript
TP1: +0.4% Close 75%
TP2: +1.0% Activate trailing stop on 25%
Runner: 25% with ATR-based trailing (current)
SL: -1.5%
```
**Option C: Quality-Based Tiers (If score correlation is strong)**
```typescript
High Quality (80-100):
TP1: +0.5% Close 50%
TP2: +1.5% Close 25%
Runner: 25% with 1.0% trailing
Medium Quality (70-79):
TP1: +0.4% Close 75%
TP2: +0.8% Close 25%
Low Quality (60-69):
TP1: +0.3% Close 100% (quick exit)
```
#### Implementation Files to Modify:
1. `config/trading.ts` - Add tier configs if using Option C
2. `lib/drift/orders.ts` - Update `placeExitOrders()` with new logic
3. `lib/trading/position-manager.ts` - Update monitoring logic
4. `app/api/trading/execute/route.ts` - Pass quality score to order placement
---
## 🔍 Current System Gaps
### 1. TradingView → n8n Integration
**Status:** ✅ Mostly working (59 trades with scores = n8n is calling execute endpoint)
**Check:** Do you have these n8n workflows?
-`Money_Machine.json` - Main trading workflow
-`parse_signal_enhanced.json` - Signal parser with metrics extraction
**Verify n8n is extracting metrics:**
- Open n8n workflow
- Check "Parse Signal Enhanced" node
- Should extract: `atr`, `adx`, `rsi`, `volumeRatio`, `pricePosition`, `timeframe`
- These get passed to `/api/trading/execute` → auto-cached
### 2. Market Data Webhook Flow
**Status:** ⚠️ Endpoint exists but no alerts feeding it
```
TradingView Alert (every 5min)
↓ POST /api/trading/market-data
Market Data Cache
↓ Used by
Manual Telegram Trades ("long sol")
```
**Currently missing:** The TradingView alerts (Priority 1 above)
---
## 📈 Success Metrics
### Phase 1 Completion Checklist:
- [ ] Market data alerts active for SOL, ETH, BTC
- [ ] Market data cache shows fresh data (<5min old)
- [ ] Manual Telegram trades show "tradingview_real" data source
- [ ] 70+ trades with signal quality scores collected
- [ ] SQL analysis completed with clear exit level recommendations
### Phase 2 Readiness:
- [ ] Clear correlation between quality score and MFE proven
- [ ] ATR correlation with move size demonstrated
- [ ] Runner viability confirmed (>40% of trades move 2%+)
- [ ] New exit strategy implemented and tested
- [ ] 10 test trades with new strategy show improvement
---
## 🚦 What to Do RIGHT NOW
**1. Setup TradingView Market Data Alerts (30 mins)**
- Follow Priority 1 steps above
- Create 3 alerts: SOL, ETH, BTC on 5min charts
- Verify cache populates after 5 minutes
**2. Test Telegram with Fresh Data (5 mins)**
```
You: "long sol"
# Should see:
✅ Data: tradingview_real (X seconds old)
Score: XX/100
```
**3. Run SQL Analysis (1 hour)**
- Execute all 4 queries from Priority 2
- Save results to a file
- Look for patterns: MFE distribution, quality correlation, runner potential
**4. Make Go/No-Go Decision**
- **IF** analysis shows clear improvements → Implement new strategy (Priority 3)
- **IF** data is unclear → Collect 20 more trades, re-analyze
- **IF** current strategy is optimal → Document findings, skip changes
**5. Optional: n8n Workflow Check**
- Verify `Money_Machine.json` includes metric extraction
- Confirm `/api/trading/check-risk` is being called
- Test manually with TradingView alert
---
## 📚 Reference Files
**Setup Guides:**
- `docs/guides/REENTRY_ANALYTICS_QUICKSTART.md` - Complete market data setup
- `docs/guides/N8N_WORKFLOW_GUIDE.md` - n8n workflow configuration
- `POSITION_SCALING_ROADMAP.md` - Full Phase 1-6 roadmap
**Analysis Queries:**
- `docs/analysis/SIGNAL_QUALITY_VERSION_ANALYSIS.sql` - Quality score deep-dive
**API Endpoints:**
- GET `/api/trading/market-data` - View cache status
- POST `/api/trading/market-data` - Update cache (from TradingView)
- POST `/api/analytics/reentry-check` - Validate manual trades
**Key Files:**
- `lib/trading/market-data-cache.ts` - Cache service (5min expiry)
- `app/api/analytics/reentry-check/route.ts` - Re-entry validation
- `telegram_command_bot.py` - Manual trade execution
---
## ❓ Questions to Answer
**For Priority 1 (TradingView Setup):**
- [ ] What's your TradingView webhook URL? (bot domain + port 3001)
- [ ] Do you want 1min or 5min bar closes? (recommend 5min to save alerts)
- [ ] Are webhooks enabled on your TradingView plan?
**For Priority 2 (Analysis):**
- [ ] What's your target win rate vs R:R trade-off preference?
- [ ] Do you prefer quick exits or letting runners develop?
- [ ] What's acceptable MAE before you want emergency exit?
**For Priority 3 (Implementation):**
- [ ] Should we implement quality-based tiers or one universal strategy?
- [ ] Keep current TP2-as-runner (25%) or go back to partial close?
- [ ] Test with DRY_RUN first or go live immediately?
---
**Bottom Line:** You're 80% done! Just need TradingView alerts configured (Priority 1) and then run the SQL analysis (Priority 2) to determine optimal exits. The infrastructure is solid and ready.

View File

@@ -0,0 +1,138 @@
# CRITICAL BUG FIX: Position Manager Size Detection
**Date:** November 8, 2025, 16:21 UTC
**Severity:** CRITICAL - TP1 detection completely broken
**Status:** FIXED
---
## 🚨 Problem Summary
The Position Manager was **NOT detecting TP1 fills** due to incorrect position size calculation, leaving traders exposed to full risk even after partial profits were taken.
---
## 💥 The Bug
**File:** `lib/trading/position-manager.ts` line 319
**BROKEN CODE:**
```typescript
const positionSizeUSD = position.size * currentPrice
```
**What it did:**
- Multiplied Drift's `position.size` by current price
- Assumed `position.size` was in tokens (SOL, ETH, etc.)
- **WRONG:** Drift SDK already returns `position.size` in USD notional value!
**Result:**
- Calculated position size: $522 (3.34 SOL × $156)
- Expected position size: $2100 (from database)
- 75% difference triggered "Position size mismatch" warnings
- **TP1 detection logic NEVER triggered**
- Stop loss never moved to breakeven
- Trader left exposed to full -1.5% risk on remaining position
---
## ✅ The Fix
**CORRECTED CODE:**
```typescript
const positionSizeUSD = Math.abs(position.size) // Drift SDK returns negative for shorts
```
**What it does now:**
- Uses Drift's position.size directly (already in USD)
- Handles negative values for short positions
- Correctly compares: $1575 (75% remaining) vs $2100 (original)
- **25% reduction properly detected as TP1 fill**
- Stop loss moves to breakeven as designed
---
## 📊 Evidence from Logs
**Before fix:**
```
⚠️ Position size mismatch: expected 522.4630506538, got 3.34
⚠️ Position size mismatch: expected 522.47954, got 3.34
```
**After fix (expected):**
```
📊 Position check: Drift=$1575.00 Tracked=$2100.00 Diff=25.0%
✅ Position size reduced: tracking $2100.00 → found $1575.00
🎯 TP1 detected as filled! Reduction: 25.0%
🛡️ Stop loss moved to breakeven: $157.34
```
---
## 🎯 Impact
**Affected:**
- ALL trades since bot v4 launch
- Position Manager never properly detected TP1 fills
- On-chain TP orders worked, but software monitoring failed
- Stop loss adjustments NEVER happened
**Trades at risk:**
- Any position where TP1 filled but bot didn't move SL
- Current open position (SOL short from 15:01)
---
## 🔄 Related Changes
Also added debug logging:
```typescript
console.log(`📊 Position check: Drift=$${positionSizeUSD.toFixed(2)} Tracked=$${trackedSizeUSD.toFixed(2)} Diff=${sizeDiffPercent.toFixed(1)}%`)
```
This will help diagnose future issues.
---
## 🚀 Deployment
```bash
cd /home/icke/traderv4
docker compose build trading-bot
docker compose up -d --force-recreate trading-bot
docker logs -f trading-bot-v4
```
Wait for next price check cycle (2 seconds) and verify:
- TP1 detection triggers
- SL moves to breakeven
- Logs show correct USD values
---
## 📝 Prevention
**Root cause:** Assumption about SDK data format without verification
**Lessons:**
1. Always verify SDK return value formats with actual data
2. Add extensive logging for financial calculations
3. Test with real trades before deploying
4. Monitor "mismatch" warnings - they indicate bugs
---
## ⚠️ Manual Intervention Needed
For the **current open position**, once bot restarts:
1. Position Manager will detect the 25% reduction
2. Automatically move SL to breakeven ($157.34)
3. Update on-chain stop loss order
4. Continue monitoring for TP2
**No manual action required** - the fix handles everything automatically!
---
**Status:** Fix deployed, container rebuilding, will be live in ~2 minutes.

191
N8N_MARKET_DATA_SETUP.md Normal file
View File

@@ -0,0 +1,191 @@
# How to Add Market Data Handler to Your n8n Workflow
## 🎯 Goal
Add logic to detect market data alerts and forward them to your bot, while keeping trading signals working normally.
---
## 📥 Method 1: Import the Pre-Built Nodes (Easier)
### Step 1: Download the File
The file is saved at: `/home/icke/traderv4/workflows/trading/market_data_handler.json`
### Step 2: Import into n8n
1. Open your **Money Machine** workflow in n8n
2. Click the **"⋮"** (three dots) menu at the top
3. Select **"Import from File"**
4. Upload `market_data_handler.json`
5. This will add the nodes to your canvas
### Step 3: Connect to Your Existing Flow
The imported nodes include:
- **Webhook** (same as your existing one)
- **Is Market Data?** (new IF node to check if it's market data)
- **Forward to Bot** (HTTP Request to your bot)
- **Respond Success** (sends 200 OK back)
- **Parse Trading Signal** (your existing logic for trading signals)
You'll need to:
1. **Delete** the duplicate Webhook node (keep your existing one)
2. **Connect** your existing Webhook → **Is Market Data?** node
3. The rest should flow automatically
---
## 🔧 Method 2: Add Manually (Step-by-Step)
If import doesn't work, add these nodes manually:
### Step 1: Add "IF" Node After Webhook
1. Click on the canvas in your Money Machine workflow
2. **Add node** → Search for **"IF"**
3. **Place it** right after your "Webhook" node
4. **Connect:** Webhook → IF node
### Step 2: Configure the IF Node
**Name:** `Is Market Data?`
**Condition:**
- **Value 1:** `={{ $json.body.action }}`
- **Operation:** equals
- **Value 2:** `market_data`
This checks if the incoming alert has `"action": "market_data"` in the JSON.
### Step 3: Add HTTP Request Node (True Branch)
When condition is TRUE (it IS market data):
1. **Add node****"HTTP Request"**
2. **Connect** from the **TRUE** output of the IF node
3. **Configure:**
- **Name:** `Forward to Bot`
- **Method:** POST
- **URL:** `http://trading-bot-v4:3000/api/trading/market-data`
- **Send Body:** Yes ✅
- **Body Content Type:** JSON
- **JSON Body:** `={{ $json.body }}`
### Step 4: Add Respond to Webhook (After HTTP Request)
1. **Add node****"Respond to Webhook"**
2. **Connect** from HTTP Request node
3. **Configure:**
- **Response Code:** 200
- **Response Body:** `{"success": true, "cached": true}`
### Step 5: Connect False Branch to Your Existing Flow
From the **FALSE** output of the IF node (NOT market data):
1. **Connect** to your existing **"Parse Signal Enhanced"** node
2. This is where your normal trading signals flow
---
## 📊 Final Flow Diagram
```
Webhook (receives all TradingView alerts)
Is Market Data? (IF node)
↓ ↓
TRUE FALSE
↓ ↓
Forward to Bot Parse Signal Enhanced
↓ ↓
Respond Success (your existing trading flow...)
```
---
## ✅ Testing
### Step 1: Update TradingView Alert
Change your market data alert webhook URL to:
```
https://flow.egonetix.de/webhook/tradingview-bot-v4
```
(This is your MAIN webhook that's already working)
### Step 2: Wait 5 Minutes
Wait for the next bar close (5 minutes max).
### Step 3: Check n8n Executions
1. Click **"Executions"** tab in n8n
2. You should see executions showing:
- Webhook triggered
- IS Market Data? = TRUE
- Forward to Bot = Success
### Step 4: Verify Bot Cache
```bash
curl http://localhost:3001/api/trading/market-data
```
Should show:
```json
{
"success": true,
"availableSymbols": ["SOL-PERP"],
"count": 1,
"cache": {
"SOL-PERP": {
"atr": 0.26,
"adx": 15.4,
"rsi": 47.3,
...
"ageSeconds": 23
}
}
}
```
---
## 🐛 Troubleshooting
**Problem: IF node always goes to FALSE**
Check the condition syntax:
- Make sure it's `={{ $json.body.action }}` (with double equals and curly braces)
- NOT `{ $json.body.action }` (single braces won't work)
**Problem: HTTP Request fails**
- Check URL is `http://trading-bot-v4:3000/api/trading/market-data`
- NOT `http://10.0.0.48:3001/...` (use Docker internal network)
- Make sure body is `={{ $json.body }}` to forward the entire JSON
**Problem: Still getting empty cache**
- Check n8n Executions tab to see if workflow is running
- Look for errors in the execution log
- Verify your TradingView alert is using the correct webhook URL
---
## 🎯 Summary
**What this does:**
1. ✅ All TradingView alerts go to same webhook
2. ✅ Market data alerts (with `"action": "market_data"`) → Forward to bot cache
3. ✅ Trading signals (without `"action": "market_data"`) → Normal trading flow
4. ✅ No need for separate webhooks
5. ✅ Uses your existing working webhook infrastructure
**After setup:**
- Trading signals continue to work normally
- Market data flows to bot cache every 5 minutes
- Manual Telegram trades get fresh data
---
**Import the JSON file or add the nodes manually, then test!** 🚀

124
QUICK_SETUP_CARD.md Normal file
View File

@@ -0,0 +1,124 @@
# Quick Reference - Your Setup Info
## ✅ Your Trading Bot Status
- **Container:** Running and healthy ✅
- **Endpoint:** Working correctly ✅
- **Server IP:** 10.0.0.48
---
## 📋 YOUR WEBHOOK URL
Use this URL in TradingView alerts:
```
http://10.0.0.48:3001/api/trading/market-data
```
**OR if you have n8n setup as proxy:**
```
https://flow.egonetix.de/webhook/market-data
```
---
## 📝 COPY-PASTE CHECKLIST
When creating EACH alert in TradingView:
### 1⃣ CONDITION
```
time("1") changes
```
### 2⃣ WEBHOOK URL
```
http://10.0.0.48:3001/api/trading/market-data
```
### 3⃣ ALERT MESSAGE (full JSON)
```json
{
"action": "market_data",
"symbol": "{{ticker}}",
"timeframe": "{{interval}}",
"atr": {{ta.atr(14)}},
"adx": {{ta.dmi(14, 14)}},
"rsi": {{ta.rsi(14)}},
"volumeRatio": {{volume / ta.sma(volume, 20)}},
"pricePosition": {{(close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100}},
"currentPrice": {{close}}
}
```
### 4⃣ SETTINGS
- **Frequency:** Once Per Bar Close
- **Expiration:** Never
- **Notifications:** ONLY ✅ Webhook URL (uncheck all others)
---
## 🎯 THE 3 ALERTS YOU NEED
| # | Symbol | Alert Name |
|---|---------|-------------------------|
| 1 | SOLUSDT | Market Data - SOL 5min |
| 2 | ETHUSDT | Market Data - ETH 5min |
| 3 | BTCUSDT | Market Data - BTC 5min |
All on 5-minute charts, all using same config above.
---
## ✅ VERIFICATION COMMAND
After creating alerts, wait 5 minutes, then run:
```bash
curl http://localhost:3001/api/trading/market-data
```
**You should see symbols appear:**
```json
{
"success": true,
"availableSymbols": ["SOL-PERP", "ETH-PERP", "BTC-PERP"],
"count": 3
}
```
---
## 🆘 IF SOMETHING GOES WRONG
**Check bot logs:**
```bash
docker logs -f trading-bot-v4
```
Watch for incoming POST requests when bar closes.
**Test from external machine:**
```bash
curl http://10.0.0.48:3001/api/trading/market-data
```
If this fails → port 3001 blocked by firewall.
---
## 📖 DETAILED GUIDE
See: `TRADINGVIEW_STEP_BY_STEP.md` for detailed walkthrough with screenshots.
---
## ⏭️ NEXT STEP
After alerts are working and cache is populated:
```bash
./scripts/run_exit_analysis.sh
```
This will analyze your trades and recommend optimal TP/SL levels.

View File

@@ -0,0 +1,127 @@
# TradingView Alert - EASIEST METHOD
Since you don't have "time()" in the condition dropdown, we'll use a **Pine Script indicator** instead. This is actually easier!
---
## STEP 1: Add the Pine Script Indicator
1. **On your SOLUSDT 5-minute chart**, click the **Pine Editor** button at bottom
- Or go to: Pine Editor tab at the bottom of the screen
2. **Delete everything** in the editor
3. **Copy and paste** this entire script:
```pinescript
//@version=5
indicator("Market Data Alert", overlay=false)
// Calculate metrics
atr_value = ta.atr(14)
adx_value = ta.dmi(14, 14)
rsi_value = ta.rsi(close, 14)
volume_ratio = volume / ta.sma(volume, 20)
price_position = (close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100
// Plot something so indicator appears
plot(1, "Signal", color=color.green)
// Alert condition
alertcondition(true, title="Market Data", message='{"action":"market_data","symbol":"{{ticker}}","timeframe":"{{interval}}","atr":' + str.tostring(atr_value) + ',"adx":' + str.tostring(adx_value) + ',"rsi":' + str.tostring(rsi_value) + ',"volumeRatio":' + str.tostring(volume_ratio) + ',"pricePosition":' + str.tostring(price_position) + ',"currentPrice":' + str.tostring(close) + '}')
```
4. **Click "Save"** button
- Name it: `Market Data Alert`
5. **Click "Add to Chart"** button
You should now see a new indicator panel at the bottom of your chart.
---
## STEP 2: Create the Alert (NOW IT'S EASY!)
1. **Right-click** on the indicator name in the legend (where it says "Market Data Alert")
2. **Select "Add Alert on Market Data Alert"**
OR
1. **Click the Alert icon** 🔔 (or press ALT + A)
2. **In the Condition dropdown**, you should now see:
- **"Market Data Alert"** → Select this
- Then select: **"Market Data"** (the alert condition name)
3. **Settings section:**
| Setting | Value |
|---------|-------|
| **Webhook URL** | `http://10.0.0.48:3001/api/trading/market-data` |
| **Alert name** | `Market Data - SOL 5min` |
| **Frequency** | **Once Per Bar Close** |
| **Expiration** | Never |
4. **Notifications:**
-**Webhook URL** (ONLY this one checked)
- ❌ Uncheck everything else
5. **Alert message:**
- **Leave it as default** (the script handles the message)
- OR if there's a message field, it should already have the JSON
6. **Click "Create"**
---
## STEP 3: Repeat for ETH and BTC
1. **Open ETHUSDT 5-minute chart**
2. **Add the same indicator** (Pine Editor → paste script → Save → Add to Chart)
3. **Create alert** on the indicator
4. **Webhook URL:** `http://10.0.0.48:3001/api/trading/market-data`
5. **Name:** `Market Data - ETH 5min`
Repeat for **BTCUSDT**.
---
## ✅ VERIFY (Wait 5 Minutes)
```bash
curl http://localhost:3001/api/trading/market-data
```
Should show all 3 symbols with fresh data.
---
## 🎯 Why This Method is Better
-**Works on all TradingView plans** (that support indicators)
-**Easier to set up** (no complex condition configuration)
-**Message is built-in** (less copy-paste errors)
-**Visual feedback** (shows metrics on chart)
-**Reusable** (same indicator for all symbols)
---
## 🐛 Troubleshooting
**"Pine Editor not available"**
- You need TradingView Pro/Premium for custom scripts
- Alternative: Use the "Crossing" method below
**Alternative without Pine Script:**
1. **Condition:** Price
2. **Trigger:** Crossing up
3. **Value:** Any value
4. **Check:** "Only once per bar close"
5. **Message:** Use the JSON from `QUICK_SETUP_CARD.md`
This will fire less frequently but still works.
---
**Try the Pine Script method first - it's the cleanest solution!** 🚀

View File

@@ -0,0 +1,243 @@
# TradingView Market Data Alert Setup
## Quick Copy-Paste Alert Configuration
### Alert 1: SOL Market Data (5-minute bars)
**Symbol:** SOLUSDT
**Timeframe:** 5 minutes
**Alert Name:** Market Data - SOL 5min
**Condition:**
```pinescript
ta.change(time("1"))
```
(This fires every bar close)
**Alert Message:**
```json
{
"action": "market_data",
"symbol": "{{ticker}}",
"timeframe": "{{interval}}",
"atr": {{ta.atr(14)}},
"adx": {{ta.dmi(14, 14)}},
"rsi": {{ta.rsi(14)}},
"volumeRatio": {{volume / ta.sma(volume, 20)}},
"pricePosition": {{(close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100}},
"currentPrice": {{close}}
}
```
**Webhook URL:** (Choose one based on your setup)
```
Option 1 (if bot is publicly accessible):
https://YOUR-DOMAIN.COM:3001/api/trading/market-data
Option 2 (if using n8n as proxy):
https://flow.egonetix.de/webhook/market-data
Option 3 (local testing):
http://YOUR-SERVER-IP:3001/api/trading/market-data
```
**Settings:**
- ✅ Webhook URL (enable and enter URL above)
- ✅ Once Per Bar Close
- Expiration: Never
- Name on chart: Market Data - SOL 5min
---
### Alert 2: ETH Market Data (5-minute bars)
**Symbol:** ETHUSDT
**Timeframe:** 5 minutes
**Alert Name:** Market Data - ETH 5min
**Condition:**
```pinescript
ta.change(time("1"))
```
**Alert Message:**
```json
{
"action": "market_data",
"symbol": "{{ticker}}",
"timeframe": "{{interval}}",
"atr": {{ta.atr(14)}},
"adx": {{ta.dmi(14, 14)}},
"rsi": {{ta.rsi(14)}},
"volumeRatio": {{volume / ta.sma(volume, 20)}},
"pricePosition": {{(close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100}},
"currentPrice": {{close}}
}
```
**Webhook URL:** (Same as SOL above)
**Settings:**
- ✅ Webhook URL (same as SOL)
- ✅ Once Per Bar Close
- Expiration: Never
---
### Alert 3: BTC Market Data (5-minute bars)
**Symbol:** BTCUSDT
**Timeframe:** 5 minutes
**Alert Name:** Market Data - BTC 5min
**Condition:**
```pinescript
ta.change(time("1"))
```
**Alert Message:**
```json
{
"action": "market_data",
"symbol": "{{ticker}}",
"timeframe": "{{interval}}",
"atr": {{ta.atr(14)}},
"adx": {{ta.dmi(14, 14)}},
"rsi": {{ta.rsi(14)}},
"volumeRatio": {{volume / ta.sma(volume, 20)}},
"pricePosition": {{(close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100}},
"currentPrice": {{close}}
}
```
**Webhook URL:** (Same as SOL above)
**Settings:**
- ✅ Webhook URL (same as SOL)
- ✅ Once Per Bar Close
- Expiration: Never
---
## Verification Steps
### Step 1: Check Webhook Endpoint is Accessible
```bash
# From your server
curl http://localhost:3001/api/trading/market-data
# Should return:
# {"success":true,"availableSymbols":[],"count":0,"cache":{}}
```
### Step 2: Wait 5 Minutes for First Alert
After creating alerts, wait for next bar close (5 minutes max)
### Step 3: Verify Cache is Populated
```bash
curl http://localhost:3001/api/trading/market-data
# Should now show:
# {
# "success": true,
# "availableSymbols": ["SOL-PERP", "ETH-PERP", "BTC-PERP"],
# "count": 3,
# "cache": {
# "SOL-PERP": {
# "atr": 0.45,
# "adx": 32.1,
# "rsi": 58.3,
# "ageSeconds": 23
# },
# ...
# }
# }
```
### Step 4: Test Telegram Trade
```
You: "long sol"
# Should see:
✅ Analytics check passed (68/100)
Data: tradingview_real (23s old) ← FRESH DATA!
Proceeding with LONG SOL...
```
---
## Troubleshooting
### Problem: Cache still empty after 10 minutes
**Check:**
1. TradingView alerts show "Active" status
2. Webhook URL is correct (check for typos)
3. Port 3001 is accessible (firewall rules)
4. Docker container is running: `docker ps | grep trading-bot`
5. Check logs: `docker logs -f trading-bot-v4`
### Problem: Alerts not firing
**Check:**
1. TradingView plan supports webhooks (Pro/Premium/Pro+)
2. Chart is open (alerts need chart loaded to fire)
3. Condition `ta.change(time("1"))` is correct
4. Timeframe matches (5-minute chart)
### Problem: JSON parse errors in logs
**Check:**
1. Alert message is valid JSON (no trailing commas)
2. TradingView placeholders use `{{ticker}}` not `{ticker}`
3. No special characters breaking JSON
---
## Alert Cost Optimization
**Current setup:** 3 alerts firing every 5 minutes = ~864 alerts/day
**TradingView Alert Limits:**
- Free: 1 alert
- Pro: 20 alerts
- Pro+: 100 alerts
- Premium: 400 alerts
**If you need to reduce alerts:**
1. Use 15-minute bars instead of 5-minute (reduces by 67%)
2. Only enable alerts for symbols you actively trade
3. Use same alert for multiple symbols (requires script modification)
---
## Advanced: n8n Proxy Setup (Optional)
If your bot is not publicly accessible, use n8n as webhook proxy:
**Step 1:** Create n8n webhook
- Webhook URL: `https://flow.egonetix.de/webhook/market-data`
- Method: POST
- Response: Return text
**Step 2:** Add HTTP Request node
- URL: `http://trading-bot-v4:3000/api/trading/market-data`
- Method: POST
- Body: `{{ $json }}`
- Headers: None needed (internal network)
**Step 3:** Use n8n URL in TradingView alerts
```
https://flow.egonetix.de/webhook/market-data
```
---
## Next: Enable Market Data Alerts
1. **Copy alert message JSON** from above
2. **Open TradingView** → SOL/USD 5-minute chart
3. **Click alert icon** (top right)
4. **Paste condition and message**
5. **Save alert**
6. **Repeat for ETH and BTC**
7. **Wait 5 minutes and verify cache**
**Once verified, proceed to SQL analysis!**

351
TRADINGVIEW_STEP_BY_STEP.md Normal file
View File

@@ -0,0 +1,351 @@
# TradingView Alert Setup - Step-by-Step Guide
## 🎯 Goal
Create 3 alerts that send market data to your trading bot every 5 minutes.
---
## STEP 1: Find Your Webhook URL
First, we need to know where to send the data.
**Check if your bot is accessible:**
```bash
# On your server, run:
curl http://localhost:3001/api/trading/market-data
```
**Expected response:**
```json
{"success":true,"availableSymbols":[],"count":0,"cache":{}}
```
**Your webhook URL will be ONE of these:**
- `http://YOUR-SERVER-IP:3001/api/trading/market-data` (if port 3001 is open)
- `https://YOUR-DOMAIN.COM:3001/api/trading/market-data` (if you have a domain)
- `https://flow.egonetix.de/webhook/market-data` (if using n8n as proxy)
**Write down your URL here:**
```
My webhook URL: ________________________________
```
---
## STEP 2: Open TradingView and Go to SOL Chart
1. **Go to:** https://www.tradingview.com
2. **Login** to your account
3. **Click** on the chart icon or search bar at top
4. **Type:** `SOLUSDT`
5. **Click** on `SOLUSDT` from Binance (or your preferred exchange)
6. **Set timeframe** to **5 minutes** (click "5" in the top toolbar)
**You should now see:** A 5-minute chart of SOLUSDT
---
## STEP 3: Create Alert
1. **Click** the **Alert icon** 🔔 in the right toolbar
- Or press **ALT + A** on keyboard
2. A popup window opens titled "Create Alert"
---
## STEP 4: Configure Alert Condition
In the "Condition" section:
1. **First dropdown:** Select `time("1")`
- Click the dropdown that says "Crossing" or whatever is there
- **Type** in the search: `time`
- Select: `time``time("1")`
2. **Second dropdown:** Select `changes`
- This should appear automatically after selecting time("1")
- If not, select "changes" from the dropdown
**What you should see:**
```
time("1") changes
```
This means: "Alert fires when time changes" = every bar close
---
## STEP 5: Set Alert Actions (IMPORTANT!)
Scroll down to the "Notifications" section:
**UNCHECK everything EXCEPT:**
-**Webhook URL** (leave this CHECKED)
**UNCHECK these:**
- ❌ Notify on app
- ❌ Show popup
- ❌ Send email
- ❌ Play sound
**We ONLY want webhook!**
---
## STEP 6: Enter Webhook URL
1. In the **Webhook URL** field, paste your URL from Step 1:
```
http://YOUR-SERVER-IP:3001/api/trading/market-data
```
*(Replace with your actual URL)*
2. **Click in the field** to make sure it's saved
---
## STEP 7: Configure Alert Message (COPY-PASTE THIS)
Scroll to the **"Alert message"** box.
**DELETE everything** in that box.
**PASTE this EXACTLY** (copy the entire JSON):
```json
{
"action": "market_data",
"symbol": "{{ticker}}",
"timeframe": "{{interval}}",
"atr": {{ta.atr(14)}},
"adx": {{ta.dmi(14, 14)}},
"rsi": {{ta.rsi(14)}},
"volumeRatio": {{volume / ta.sma(volume, 20)}},
"pricePosition": {{(close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100}},
"currentPrice": {{close}}
}
```
**IMPORTANT:** Make sure:
- No spaces added/removed
- The `{{` double brackets are kept
- No missing commas
- No extra text
---
## STEP 8: Set Alert Name and Frequency
**Alert name:**
```
Market Data - SOL 5min
```
**Frequency section:**
- Select: **"Once Per Bar Close"**
- NOT "Only Once"
- NOT "Once Per Bar"
- Must be **"Once Per Bar Close"**
**Expiration:**
- Select: **"Never"** or **"Open-ended"**
**Show popup / Name on chart:**
- You can uncheck these (optional)
---
## STEP 9: Create the Alert
1. **Click** the blue **"Create"** button at bottom
2. Alert is now active! ✅
You should see it in your alerts list (🔔 icon in right panel)
---
## STEP 10: Repeat for ETH and BTC
Now do the EXACT same steps for:
**For ETH:**
1. Search for `ETHUSDT`
2. Set to 5-minute chart
3. Create alert (ALT + A)
4. Condition: `time("1") changes`
5. Webhook URL: (same as SOL)
6. Alert message: (same JSON as SOL)
7. Alert name: `Market Data - ETH 5min`
8. Frequency: Once Per Bar Close
9. Create
**For BTC:**
1. Search for `BTCUSDT`
2. Set to 5-minute chart
3. Create alert (ALT + A)
4. Condition: `time("1") changes`
5. Webhook URL: (same as SOL)
6. Alert message: (same JSON as SOL)
7. Alert name: `Market Data - BTC 5min`
8. Frequency: Once Per Bar Close
9. Create
---
## ✅ VERIFICATION (Wait 5 Minutes)
After creating alerts, **WAIT UP TO 5 MINUTES** for the next bar close.
Then run this on your server:
```bash
curl http://localhost:3001/api/trading/market-data
```
**You should see:**
```json
{
"success": true,
"availableSymbols": ["SOL-PERP", "ETH-PERP", "BTC-PERP"],
"count": 3,
"cache": {
"SOL-PERP": {
"atr": 0.45,
"adx": 32.1,
"rsi": 58.3,
"volumeRatio": 1.25,
"pricePosition": 55.2,
"ageSeconds": 23
},
...
}
}
```
**If you see this → SUCCESS!** ✅
---
## 🐛 Troubleshooting
### Problem: Still shows empty cache after 10 minutes
**Check 1: Are alerts active?**
- Click 🔔 icon in TradingView
- Look for your 3 alerts
- They should say "Active" (not paused)
**Check 2: Is webhook URL correct?**
- Click on an alert to edit it
- Check the Webhook URL field
- No typos? Correct port (3001)?
**Check 3: Check bot logs**
```bash
docker logs -f trading-bot-v4
```
Wait for next bar close and watch for incoming requests.
You should see:
```
POST /api/trading/market-data
✅ Market data cached for SOL-PERP
```
**Check 4: Is port 3001 open?**
```bash
# From another machine or phone:
curl http://YOUR-SERVER-IP:3001/api/trading/market-data
```
If this fails, port 3001 might be blocked by firewall.
**Check 5: TradingView plan supports webhooks?**
- Free plan: NO webhooks ❌
- Pro plan: YES ✅
- Pro+ plan: YES ✅
- Premium: YES ✅
If you have Free plan, you need to upgrade to Pro ($14.95/month).
---
## 📸 Visual Guide
**Where is the Alert icon?**
```
TradingView Chart:
┌─────────────────────────────────────┐
│ [Chart toolbar at top] │
│ │
│ [Chart area] 🔔 ← Alert icon (right side)
│ │
│ │
└─────────────────────────────────────┘
```
**What the Alert popup looks like:**
```
┌─ Create Alert ────────────────┐
│ │
│ Condition: │
│ [time("1")] [changes] │
│ │
│ Notifications: │
│ ✅ Webhook URL │
│ [http://your-url...] │
│ │
│ Alert message: │
│ [{"action":"market_data",...}]│
│ │
│ Alert name: │
│ [Market Data - SOL 5min] │
│ │
│ Frequency: │
│ [Once Per Bar Close] │
│ │
│ [Create] │
└───────────────────────────────┘
```
---
## 🎬 Quick Recap
**You need to create 3 alerts total:**
| Symbol | Chart | Alert Name | Frequency |
|-----------|-----------|-------------------------|-------------------|
| SOLUSDT | 5-minute | Market Data - SOL 5min | Once Per Bar Close|
| ETHUSDT | 5-minute | Market Data - ETH 5min | Once Per Bar Close|
| BTCUSDT | 5-minute | Market Data - BTC 5min | Once Per Bar Close|
**All 3 use:**
- Same webhook URL
- Same alert message (the JSON)
- Same condition: `time("1") changes`
- Same frequency: Once Per Bar Close
**After 5 minutes:**
- Check cache is populated
- Test with Telegram: `long sol`
---
## ❓ Still Stuck?
**Common mistakes:**
1. ❌ Using "Once Per Bar" instead of "Once Per Bar Close"
2. ❌ Alert message has extra spaces or missing brackets
3. ❌ Webhook URL has typo or wrong port
4. ❌ Alert is paused (not active)
5. ❌ Free TradingView plan (needs Pro for webhooks)
**Need help?**
- Show me a screenshot of your alert configuration
- Show me the output of `docker logs trading-bot-v4`
- Show me the output of `curl http://localhost:3001/api/trading/market-data`
---
**Once alerts are working, you're ready to run the SQL analysis!** 🚀

View File

@@ -0,0 +1,51 @@
import { NextRequest, NextResponse } from 'next/server'
import { cancelAllOrders } from '@/lib/drift/orders'
import { initializeDriftService } from '@/lib/drift/client'
/**
* Cancel all orders for a symbol
* POST /api/trading/cancel-orders
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { symbol } = body
if (!symbol) {
return NextResponse.json(
{ success: false, error: 'Symbol required' },
{ status: 400 }
)
}
console.log(`🗑️ Manual order cancellation requested for ${symbol}`)
// Initialize Drift service
await initializeDriftService()
// Cancel all orders
const result = await cancelAllOrders(symbol)
if (result.success) {
return NextResponse.json({
success: true,
message: `Cancelled ${result.cancelledCount || 0} orders for ${symbol}`,
cancelledCount: result.cancelledCount,
})
} else {
return NextResponse.json(
{ success: false, error: result.error },
{ status: 500 }
)
}
} catch (error) {
console.error('❌ Error cancelling orders:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to cancel orders',
},
{ status: 500 }
)
}
}

View File

@@ -624,6 +624,33 @@ export async function closePosition(
/**
* Cancel all open orders for a specific market
*/
/**
* Retry a function with exponential backoff for rate limit errors
*/
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 2000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const isRateLimit = errorMessage.includes('429') || errorMessage.includes('rate limit')
if (!isRateLimit || attempt === maxRetries) {
throw error
}
const delay = baseDelay * Math.pow(2, attempt)
console.log(`⏳ Rate limited, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${maxRetries})`)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw new Error('Max retries reached')
}
export async function cancelAllOrders(
symbol: string
): Promise<{ success: boolean; cancelledCount?: number; error?: string }> {
@@ -667,12 +694,14 @@ export async function cancelAllOrders(
console.log(`📋 Found ${ordersToCancel.length} open orders to cancel (including trigger orders)`)
// Cancel all orders for this market (cancels all types: LIMIT, TRIGGER_MARKET, TRIGGER_LIMIT)
const txSig = await driftClient.cancelOrders(
undefined, // Cancel by market type
marketConfig.driftMarketIndex,
undefined // No specific direction filter
)
// Cancel all orders with retry logic for rate limits
const txSig = await retryWithBackoff(async () => {
return await driftClient.cancelOrders(
undefined, // Cancel by market type
marketConfig.driftMarketIndex,
undefined // No specific direction filter
)
})
console.log(`✅ Orders cancelled! Transaction: ${txSig}`)

View File

@@ -316,13 +316,16 @@ export class PositionManager {
console.log(`⚠️ Position ${trade.symbol} was closed externally (by on-chain order)`)
} else {
// Position exists - check if size changed (TP1/TP2 filled)
const positionSizeUSD = position.size * currentPrice
// CRITICAL FIX: position.size from Drift SDK is already in USD notional value
const positionSizeUSD = Math.abs(position.size) // Drift SDK returns negative for shorts
const trackedSizeUSD = trade.currentSize
const sizeDiffPercent = Math.abs(positionSizeUSD - trackedSizeUSD) / trackedSizeUSD * 100
console.log(`📊 Position check: Drift=$${positionSizeUSD.toFixed(2)} Tracked=$${trackedSizeUSD.toFixed(2)} Diff=${sizeDiffPercent.toFixed(1)}%`)
// If position size reduced significantly, TP orders likely filled
if (positionSizeUSD < trackedSizeUSD * 0.9 && sizeDiffPercent > 10) {
console.log(`📊 Position size changed: tracking $${trackedSizeUSD.toFixed(2)} but found $${positionSizeUSD.toFixed(2)}`)
console.log(` Position size reduced: tracking $${trackedSizeUSD.toFixed(2)} found $${positionSizeUSD.toFixed(2)}`)
// Detect which TP filled based on size reduction
const reductionPercent = ((trackedSizeUSD - positionSizeUSD) / trade.positionSize) * 100
@@ -424,7 +427,10 @@ export class PositionManager {
// trade.currentSize may already be 0 if on-chain orders closed the position before
// Position Manager detected it, causing zero P&L bug
// HOWEVER: If this was a phantom trade (extreme size mismatch), set P&L to 0
const sizeForPnL = trade.currentSize > 0 ? trade.currentSize : trade.positionSize
// CRITICAL FIX: Use tp1Hit flag to determine which size to use for P&L calculation
// - If tp1Hit=false: First closure, calculate on full position size
// - If tp1Hit=true: Runner closure, calculate on tracked remaining size
const sizeForPnL = trade.tp1Hit ? trade.currentSize : trade.positionSize
// Check if this was a phantom trade by looking at the last known on-chain size
// If last on-chain size was <50% of expected, this is a phantom
@@ -433,7 +439,8 @@ export class PositionManager {
console.log(`📊 External closure detected - Position size tracking:`)
console.log(` Original size: $${trade.positionSize.toFixed(2)}`)
console.log(` Tracked current size: $${trade.currentSize.toFixed(2)}`)
console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)}`)
console.log(` TP1 hit: ${trade.tp1Hit}`)
console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)} (${trade.tp1Hit ? 'runner' : 'full position'})`)
if (wasPhantom) {
console.log(` ⚠️ PHANTOM TRADE: Setting P&L to 0 (size mismatch >50%)`)
}
@@ -511,6 +518,41 @@ export class PositionManager {
if (position.size < trade.currentSize * 0.95) { // 5% tolerance
console.log(`⚠️ Position size mismatch: expected ${trade.currentSize}, got ${position.size}`)
// CRITICAL: Check if position direction changed (signal flip, not TP1!)
const positionDirection = position.side === 'long' ? 'long' : 'short'
if (positionDirection !== trade.direction) {
console.log(`🔄 DIRECTION CHANGE DETECTED: ${trade.direction}${positionDirection}`)
console.log(` This is a signal flip, not TP1! Closing old position as manual.`)
// Calculate actual P&L on full position
const profitPercent = this.calculateProfitPercent(trade.entryPrice, currentPrice, trade.direction)
const actualPnL = (trade.positionSize * profitPercent) / 100
try {
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
await updateTradeExit({
positionId: trade.positionId,
exitPrice: currentPrice,
exitReason: 'manual',
realizedPnL: actualPnL,
exitOrderTx: 'SIGNAL_FLIP',
holdTimeSeconds,
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
maxGain: Math.max(0, trade.maxFavorableExcursion),
maxFavorableExcursion: trade.maxFavorableExcursion,
maxAdverseExcursion: trade.maxAdverseExcursion,
maxFavorablePrice: trade.maxFavorablePrice,
maxAdversePrice: trade.maxAdversePrice,
})
console.log(`💾 Signal flip closure recorded: P&L $${actualPnL.toFixed(2)}`)
} catch (dbError) {
console.error('❌ Failed to save signal flip closure:', dbError)
}
await this.removeTrade(trade.id)
return
}
// CRITICAL: If mismatch is extreme (>50%), this is a phantom trade
const sizeRatio = (position.size * currentPrice) / trade.currentSize
if (sizeRatio < 0.5) {

View File

@@ -0,0 +1,236 @@
-- Optimal Exit Level Analysis
-- Run this to determine data-driven SL/TP settings
-- Execute: docker exec trading-bot-postgres psql -U postgres -d trading_bot_v4 -f /path/to/this/file.sql
\echo '=========================================='
\echo '1. MFE/MAE DISTRIBUTION ANALYSIS'
\echo 'Where do trades actually move?'
\echo '=========================================='
SELECT
direction,
COUNT(*) as total_trades,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_best_profit,
ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as q25_mfe,
ROUND(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as median_mfe,
ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as q75_mfe,
ROUND(MAX("maxFavorableExcursion")::numeric, 2) as max_mfe,
ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_worst_loss,
ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "maxAdverseExcursion")::numeric, 2) as q25_mae,
ROUND(MIN("maxAdverseExcursion")::numeric, 2) as min_mae
FROM "Trade"
WHERE "exitReason" IS NOT NULL
AND "maxFavorableExcursion" IS NOT NULL
AND "maxAdverseExcursion" IS NOT NULL
GROUP BY direction;
\echo ''
\echo 'Interpretation:'
\echo '- median_mfe = Where 50% of trades profit reaches (set TP2 here)'
\echo '- q75_mfe = Where top 25% reaches (runner territory)'
\echo '- q25_mae = Where 25% of worst losses occur (set SL here + buffer)'
\echo ''
\echo '=========================================='
\echo '2. QUALITY SCORE vs PERFORMANCE'
\echo 'Do high quality signals move further?'
\echo '=========================================='
SELECT
CASE
WHEN "signalQualityScore" >= 80 THEN 'High (80-100)'
WHEN "signalQualityScore" >= 70 THEN 'Medium (70-79)'
ELSE 'Low (60-69)'
END as quality_tier,
COUNT(*) as trades,
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe,
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate,
-- How many went beyond current TP1 (+0.4%)?
ROUND(100.0 * SUM(CASE WHEN "maxFavorableExcursion" > 0.4 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as pct_exceeded_tp1,
-- How many went beyond current TP2 (+0.7%)?
ROUND(100.0 * SUM(CASE WHEN "maxFavorableExcursion" > 0.7 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as pct_exceeded_tp2,
-- How many reached runner territory (2%+)?
ROUND(100.0 * SUM(CASE WHEN "maxFavorableExcursion" > 2.0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as pct_runner_potential
FROM "Trade"
WHERE "signalQualityScore" IS NOT NULL
AND "exitReason" IS NOT NULL
AND "maxFavorableExcursion" IS NOT NULL
GROUP BY quality_tier
ORDER BY quality_tier;
\echo ''
\echo 'Interpretation:'
\echo '- If High Quality has higher pct_exceeded_tp2 → Use quality-based tiers'
\echo '- If pct_runner_potential > 40% → Runners make sense'
\echo '- If pct_runner_potential < 20% → Quick exits better'
\echo ''
\echo '=========================================='
\echo '3. RUNNER POTENTIAL BY EXIT REASON'
\echo 'Are we leaving money on the table?'
\echo '=========================================='
SELECT
direction,
"exitReason",
COUNT(*) as count,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe,
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl,
ROUND(AVG(("maxFavorableExcursion" - ABS(("exitPrice" - "entryPrice") / "entryPrice" * 100)))::numeric, 2) as avg_mfe_vs_exit_gap,
SUM(CASE WHEN "maxFavorableExcursion" > 1.5 THEN 1 ELSE 0 END) as moved_beyond_1_5pct,
SUM(CASE WHEN "maxFavorableExcursion" > 2.0 THEN 1 ELSE 0 END) as moved_beyond_2pct,
SUM(CASE WHEN "maxFavorableExcursion" > 3.0 THEN 1 ELSE 0 END) as moved_beyond_3pct
FROM "Trade"
WHERE "exitReason" IS NOT NULL
AND "maxFavorableExcursion" IS NOT NULL
AND "exitPrice" IS NOT NULL
GROUP BY direction, "exitReason"
ORDER BY direction, count DESC;
\echo ''
\echo 'Interpretation:'
\echo '- avg_mfe_vs_exit_gap = How much profit left on table'
\echo '- If TP1/TP2 exits have large gap → Move targets wider'
\echo '- If SL exits have positive MFE → SL too tight'
\echo ''
\echo '=========================================='
\echo '4. ATR CORRELATION WITH MOVEMENT'
\echo 'Does volatility predict move size?'
\echo '=========================================='
SELECT
CASE
WHEN atr < 0.3 THEN 'Low (<0.3%)'
WHEN atr < 0.6 THEN 'Medium (0.3-0.6%)'
ELSE 'High (>0.6%)'
END as atr_bucket,
COUNT(*) as trades,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe,
ROUND(AVG(ABS("maxAdverseExcursion"))::numeric, 2) as avg_mae_abs,
ROUND(AVG(atr)::numeric, 3) as avg_atr,
-- MFE to ATR ratio (how many ATRs did price move?)
ROUND(AVG("maxFavorableExcursion" / atr)::numeric, 1) as mfe_to_atr_ratio
FROM "Trade"
WHERE atr IS NOT NULL
AND atr > 0
AND "exitReason" IS NOT NULL
AND "maxFavorableExcursion" IS NOT NULL
GROUP BY atr_bucket
ORDER BY avg_atr;
\echo ''
\echo 'Interpretation:'
\echo '- mfe_to_atr_ratio = How many ATRs price typically moves'
\echo '- If ratio > 3 → Price moves 3x ATR, use ATR-based targets'
\echo '- If ratio ~1-2 → Fixed targets may work better'
\echo ''
\echo '=========================================='
\echo '5. DIRECTION-SPECIFIC PERFORMANCE'
\echo 'Should longs vs shorts have different exits?'
\echo '=========================================='
SELECT
direction,
COUNT(*) as trades,
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate,
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe,
ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae,
ROUND(AVG("holdTimeSeconds")::numeric / 60, 1) as avg_hold_minutes,
-- How many hit TP vs SL?
SUM(CASE WHEN "exitReason" IN ('TP1', 'TP2') THEN 1 ELSE 0 END) as tp_exits,
SUM(CASE WHEN "exitReason" IN ('SL', 'SOFT_SL', 'HARD_SL') THEN 1 ELSE 0 END) as sl_exits
FROM "Trade"
WHERE "exitReason" IS NOT NULL
AND "maxFavorableExcursion" IS NOT NULL
GROUP BY direction;
\echo ''
\echo 'Interpretation:'
\echo '- If one direction has much higher MFE → Give it wider TP2'
\echo '- If one direction has worse MAE → Tighten its SL'
\echo '- Different avg_hold_minutes → May need different trailing stops'
\echo ''
\echo '=========================================='
\echo '6. CURRENT TP/SL HIT ANALYSIS'
\echo 'How often do current levels trigger?'
\echo '=========================================='
SELECT
CASE
WHEN "maxFavorableExcursion" < 0.4 THEN 'Never reached TP1'
WHEN "maxFavorableExcursion" >= 0.4 AND "maxFavorableExcursion" < 0.7 THEN 'Reached TP1 only'
WHEN "maxFavorableExcursion" >= 0.7 AND "maxFavorableExcursion" < 2.0 THEN 'Reached TP2'
ELSE 'Runner territory (2%+)'
END as price_action,
COUNT(*) as trades,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER ()::numeric, 1) as pct_of_total,
ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe,
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl
FROM "Trade"
WHERE "exitReason" IS NOT NULL
AND "maxFavorableExcursion" IS NOT NULL
GROUP BY price_action
ORDER BY avg_mfe;
\echo ''
\echo 'Interpretation:'
\echo '- If most trades in "Never reached TP1" → TP1 too wide'
\echo '- If most trades in "Runner territory" → We are exiting too early'
\echo '- Ideal: ~60% reach TP1, ~30% reach TP2, ~10% runners'
\echo ''
\echo '=========================================='
\echo '7. RECENT PERFORMANCE TREND'
\echo 'Is strategy improving over time?'
\echo '=========================================='
SELECT
DATE("createdAt") as trade_date,
COUNT(*) as trades,
ROUND(AVG("signalQualityScore")::numeric, 1) as avg_score,
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate,
ROUND(SUM("realizedPnL")::numeric, 2) as daily_pnl
FROM "Trade"
WHERE "exitReason" IS NOT NULL
AND "createdAt" > NOW() - INTERVAL '30 days'
GROUP BY trade_date
ORDER BY trade_date DESC
LIMIT 10;
\echo ''
\echo 'Interpretation:'
\echo '- Rising avg_score + rising win_rate → Quality scoring working'
\echo '- Declining daily_pnl despite good win_rate → Exits may need adjustment'
\echo ''
\echo '=========================================='
\echo 'SUMMARY RECOMMENDATIONS'
\echo '=========================================='
\echo 'Based on the analysis above, consider:'
\echo ''
\echo '1. TP1 Setting:'
\echo ' - Should be at ~25th percentile of MFE (where most trades reach)'
\echo ' - Currently: +0.4%'
\echo ''
\echo '2. TP2 Setting:'
\echo ' - Should be at ~median MFE (where 50% reach)'
\echo ' - Currently: +0.7% (with ATR-based dynamic up to 3%)'
\echo ''
\echo '3. Runner Strategy:'
\echo ' - Only use if >40% trades reach "Runner territory"'
\echo ' - Currently: 25% position with ATR trailing'
\echo ''
\echo '4. Stop Loss:'
\echo ' - Should be at ~25th percentile of MAE (protect from worst 25%)'
\echo ' - Currently: -1.5% soft, -2.5% hard'
\echo ''
\echo '5. Quality-Based Tiers:'
\echo ' - Only implement if High Quality tier has significantly better metrics'
\echo ' - Check pct_runner_potential difference across tiers'
\echo ''

23
scripts/run_exit_analysis.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Run optimal exit analysis and save results
# Usage: ./run_exit_analysis.sh
OUTPUT_FILE="exit_analysis_results_$(date +%Y%m%d_%H%M%S).txt"
echo "🔍 Running optimal exit level analysis..."
echo "Results will be saved to: $OUTPUT_FILE"
echo ""
# Run the SQL analysis
docker exec trading-bot-postgres psql -U postgres -d trading_bot_v4 -f scripts/analyze_optimal_exits.sql | tee "$OUTPUT_FILE"
echo ""
echo "✅ Analysis complete!"
echo "📊 Results saved to: $OUTPUT_FILE"
echo ""
echo "Next steps:"
echo "1. Review the output above"
echo "2. Look for patterns in MFE/MAE distribution"
echo "3. Check if quality score correlation is strong"
echo "4. Decide on optimal TP1/TP2/SL levels"
echo "5. Update config/trading.ts with new settings"

View File

@@ -19,12 +19,32 @@
},
{
"parameters": {
"jsCode": "// Get the body - it might be a string or nested in an object\nlet body = $json.body || $json.query?.body || JSON.stringify($json);\n\n// If body is an object, stringify it\nif (typeof body === 'object') {\n body = JSON.stringify(body);\n}\n\n// Parse basic signal (existing logic)\nconst symbolMatch = body.match(/\\b(SOL|BTC|ETH)/i);\nconst symbol = symbolMatch ? symbolMatch[1].toUpperCase() + '-PERP' : 'SOL-PERP';\n\nconst direction = body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long';\n\n// Updated regex to match new format: \"ETH buy 15\" (no .P)\nconst timeframeMatch = body.match(/\\b(buy|sell)\\s+(\\d+|D|W|M)\\b/i);\nconst timeframe = timeframeMatch ? timeframeMatch[2] : '5';\n\n// Parse new context metrics from enhanced format:\n// \"ETH buy 15 | ATR:1.85 | ADX:28.3 | RSI:62.5 | VOL:1.45 | POS:75.3\"\nconst atrMatch = body.match(/ATR:([\\d.]+)/);\nconst atr = atrMatch ? parseFloat(atrMatch[1]) : 0;\n\nconst adxMatch = body.match(/ADX:([\\d.]+)/);\nconst adx = adxMatch ? parseFloat(adxMatch[1]) : 0;\n\nconst rsiMatch = body.match(/RSI:([\\d.]+)/);\nconst rsi = rsiMatch ? parseFloat(rsiMatch[1]) : 0;\n\nconst volumeMatch = body.match(/VOL:([\\d.]+)/);\nconst volumeRatio = volumeMatch ? parseFloat(volumeMatch[1]) : 0;\n\nconst pricePositionMatch = body.match(/POS:([\\d.]+)/);\nconst pricePosition = pricePositionMatch ? parseFloat(pricePositionMatch[1]) : 0;\n\nreturn {\n rawMessage: body,\n symbol,\n direction,\n timeframe,\n // New context fields\n atr,\n adx,\n rsi,\n volumeRatio,\n pricePosition\n};"
"fields": {
"values": [
{
"name": "rawMessage",
"stringValue": "={{ $json.body }}"
},
{
"name": "symbol",
"stringValue": "={{ $json.body.match(/\\bSOL\\b/i) ? 'SOL-PERP' : ($json.body.match(/\\bBTC\\b/i) ? 'BTC-PERP' : ($json.body.match(/\\bETH\\b/i) ? 'ETH-PERP' : 'SOL-PERP')) }}"
},
{
"name": "direction",
"stringValue": "={{ $json.body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long' }}"
},
{
"name": "timeframe",
"stringValue": "={{ $json.body.match(/\\.P\\s+(\\d+)/)?.[1] || '15' }}"
}
]
},
"options": {}
},
"id": "97d5b0ad-d078-411f-8f34-c9a81d18d921",
"name": "Parse Signal",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
-760,
580
@@ -71,7 +91,7 @@
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"symbol\": \"{{ $json.symbol }}\",\n \"direction\": \"{{ $json.direction }}\",\n \"atr\": {{ $json.atr || 0 }},\n \"adx\": {{ $json.adx || 0 }},\n \"rsi\": {{ $json.rsi || 0 }},\n \"volumeRatio\": {{ $json.volumeRatio || 0 }},\n \"pricePosition\": {{ $json.pricePosition || 0 }}\n}",
"jsonBody": "={\n \"symbol\": \"{{ $json.symbol }}\",\n \"direction\": \"{{ $json.direction }}\"\n}",
"options": {}
},
"id": "c1165de4-2095-4f5f-b9b1-18e76fd8c47b",
@@ -130,7 +150,7 @@
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal').item.json.timeframe }}\",\n \"signalStrength\": \"strong\",\n \"atr\": {{ $('Parse Signal').item.json.atr }},\n \"adx\": {{ $('Parse Signal').item.json.adx }},\n \"rsi\": {{ $('Parse Signal').item.json.rsi }},\n \"volumeRatio\": {{ $('Parse Signal').item.json.volumeRatio }},\n \"pricePosition\": {{ $('Parse Signal').item.json.pricePosition }}\n}",
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal').item.json.timeframe }}\",\n \"signalStrength\": \"strong\"\n}",
"options": {
"timeout": 120000
}
@@ -402,7 +422,7 @@
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal Enhanced').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal Enhanced').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal Enhanced').item.json.timeframe }}\",\n \"signalStrength\": \"strong\",\n \"atr\": {{ $('Parse Signal Enhanced').item.json.atr }},\n \"adx\": {{ $('Parse Signal Enhanced').item.json.adx }},\n \"rsi\": {{ $('Parse Signal Enhanced').item.json.rsi }},\n \"volumeRatio\": {{ $('Parse Signal Enhanced').item.json.volumeRatio }},\n \"pricePosition\": {{ $('Parse Signal Enhanced').item.json.pricePosition }},\n \"qualityScore\": {{ $input.first().json.qualityScore }}\n}",
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal Enhanced').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal Enhanced').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal Enhanced').item.json.timeframe }}\",\n \"signalStrength\": \"{{ $('Parse Signal Enhanced').item.json.signalStrength }}\",\n \"atr\": {{ $('Parse Signal Enhanced').item.json.atr }},\n \"adx\": {{ $('Parse Signal Enhanced').item.json.adx }},\n \"rsi\": {{ $('Parse Signal Enhanced').item.json.rsi }},\n \"volumeRatio\": {{ $('Parse Signal Enhanced').item.json.volumeRatio }},\n \"pricePosition\": {{ $('Parse Signal Enhanced').item.json.pricePosition }}\n}",
"options": {
"timeout": 120000
}
@@ -460,6 +480,21 @@
"name": "Header Auth account"
}
}
},
{
"parameters": {
"path": "c034de5f-bcd5-4470-a193-8a16fbfb73eb",
"options": {}
},
"id": "ff525977-6e95-4e45-b742-cedb5f36b4b4",
"name": "Webhook1",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-1020,
860
],
"webhookId": "c034de5f-bcd5-4470-a193-8a16fbfb73eb"
}
],
"pinData": {},
@@ -633,7 +668,7 @@
"settings": {
"executionOrder": "v1"
},
"versionId": "1ec420e9-a965-48bd-8f29-e912aa569431",
"versionId": "955bd768-0c3b-490a-9c6b-5c01bc2f6d44",
"id": "gUDqTiHyHSfRUXv6",
"meta": {
"instanceId": "e766d4f0b5def8ee8cb8561cd9d2b9ba7733e1907990b6987bca40175f82c379"

View File

@@ -0,0 +1,32 @@
//@version=5
indicator("Market Data Alert Helper", overlay=false)
// This indicator fires an alert on every bar close
// Used to send market data to trading bot
// Calculate metrics
atr_value = ta.atr(14)
adx_value = ta.dmi(14, 14)
rsi_value = ta.rsi(close, 14)
volume_ratio = volume / ta.sma(volume, 20)
price_position = (close - ta.lowest(low, 100)) / (ta.highest(high, 100) - ta.lowest(low, 100)) * 100
// Plot a simple line (just so indicator shows on chart)
plot(1, "Alert Signal", color=color.green)
// Display values on chart for verification
var table infoTable = table.new(position.top_right, 2, 6)
if barstate.islast
table.cell(infoTable, 0, 0, "ATR", text_color=color.white)
table.cell(infoTable, 1, 0, str.tostring(atr_value, "#.##"), text_color=color.white)
table.cell(infoTable, 0, 1, "ADX", text_color=color.white)
table.cell(infoTable, 1, 1, str.tostring(adx_value, "#.#"), text_color=color.white)
table.cell(infoTable, 0, 2, "RSI", text_color=color.white)
table.cell(infoTable, 1, 2, str.tostring(rsi_value, "#.#"), text_color=color.white)
table.cell(infoTable, 0, 3, "Vol Ratio", text_color=color.white)
table.cell(infoTable, 1, 3, str.tostring(volume_ratio, "#.##"), text_color=color.white)
table.cell(infoTable, 0, 4, "Price Pos", text_color=color.white)
table.cell(infoTable, 1, 4, str.tostring(price_position, "#.#"), text_color=color.white)
// Alert condition - triggers on every bar close
alertcondition(true, title="Market Data Update", message='{"action":"market_data","symbol":"{{ticker}}","timeframe":"{{interval}}","atr":' + str.tostring(atr_value) + ',"adx":' + str.tostring(adx_value) + ',"rsi":' + str.tostring(rsi_value) + ',"volumeRatio":' + str.tostring(volume_ratio) + ',"pricePosition":' + str.tostring(price_position) + ',"currentPrice":' + str.tostring(close) + '}')

View File

@@ -0,0 +1,159 @@
{
"name": "Market Data Handler - Import This",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "tradingview-bot-v4",
"options": {}
},
"id": "webhook-main",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
240,
300
],
"webhookId": "tradingview-bot-v4"
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.body.action }}",
"operation": "equals",
"value2": "market_data"
}
]
}
},
"id": "check-if-market-data",
"name": "Is Market Data?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
460,
300
]
},
{
"parameters": {
"method": "POST",
"url": "http://trading-bot-v4:3000/api/trading/market-data",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.body }}",
"options": {}
},
"id": "forward-to-bot",
"name": "Forward to Bot",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
680,
200
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={ \"success\": true, \"cached\": true }",
"options": {}
},
"id": "respond-success",
"name": "Respond Success",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
900,
200
]
},
{
"parameters": {
"fields": {
"values": [
{
"name": "rawMessage",
"stringValue": "={{ $json.body }}"
},
{
"name": "symbol",
"stringValue": "={{ $json.body.match(/\\bSOL\\b/i) ? 'SOL-PERP' : ($json.body.match(/\\bBTC\\b/i) ? 'BTC-PERP' : ($json.body.match(/\\bETH\\b/i) ? 'ETH-PERP' : 'SOL-PERP')) }}"
},
{
"name": "direction",
"stringValue": "={{ $json.body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long' }}"
},
{
"name": "timeframe",
"stringValue": "={{ $json.body.match(/\\.P\\s+(\\d+)/)?.[1] || '15' }}"
}
]
},
"options": {}
},
"id": "parse-trading-signal",
"name": "Parse Trading Signal",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
680,
400
]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Is Market Data?",
"type": "main",
"index": 0
}
]
]
},
"Is Market Data?": {
"main": [
[
{
"node": "Forward to Bot",
"type": "main",
"index": 0
}
],
[
{
"node": "Parse Trading Signal",
"type": "main",
"index": 0
}
]
]
},
"Forward to Bot": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 0,
"updatedAt": "2025-11-08T00:00:00.000Z",
"versionId": "market-data-handler-v1"
}