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:
368
ANALYTICS_STATUS_AND_NEXT_STEPS.md
Normal file
368
ANALYTICS_STATUS_AND_NEXT_STEPS.md
Normal 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.
|
||||
138
CRITICAL_FIX_POSITION_SIZE_BUG.md
Normal file
138
CRITICAL_FIX_POSITION_SIZE_BUG.md
Normal 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
191
N8N_MARKET_DATA_SETUP.md
Normal 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
124
QUICK_SETUP_CARD.md
Normal 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.
|
||||
127
TRADINGVIEW_EASIEST_METHOD.md
Normal file
127
TRADINGVIEW_EASIEST_METHOD.md
Normal 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!** 🚀
|
||||
243
TRADINGVIEW_MARKET_DATA_ALERTS.md
Normal file
243
TRADINGVIEW_MARKET_DATA_ALERTS.md
Normal 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
351
TRADINGVIEW_STEP_BY_STEP.md
Normal 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!** 🚀
|
||||
51
app/api/trading/cancel-orders/route.ts
Normal file
51
app/api/trading/cancel-orders/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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}`)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
236
scripts/analyze_optimal_exits.sql
Normal file
236
scripts/analyze_optimal_exits.sql
Normal 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
23
scripts/run_exit_analysis.sh
Executable 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"
|
||||
@@ -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"
|
||||
|
||||
32
workflows/trading/market_data_alert.pine
Normal file
32
workflows/trading/market_data_alert.pine
Normal 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) + '}')
|
||||
159
workflows/trading/market_data_handler.json
Normal file
159
workflows/trading/market_data_handler.json
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user