CRITICAL FIX: Use ?? instead of || for tp2SizePercent to allow 0 value
BUG FOUND: Line 558: tp2SizePercent: config.takeProfit2SizePercent || 100 When config.takeProfit2SizePercent = 0 (TP2-as-runner system), JavaScript's || operator treats 0 as falsy and falls back to 100, causing TP2 to close 100% of remaining position instead of activating trailing stop. IMPACT: - On-chain orders placed correctly (line 481 uses ?? correctly) - Position Manager reads from DB and expects TP2 to close position - Result: User sees TWO take-profit orders instead of runner system FIX: Changed both tp1SizePercent and tp2SizePercent to use ?? operator: - tp1SizePercent: config.takeProfit1SizePercent ?? 75 - tp2SizePercent: config.takeProfit2SizePercent ?? 0 This allows 0 value to be saved correctly for TP2-as-runner system. VERIFICATION NEEDED: Current open SHORT position in database has tp2SizePercent=100 from before this fix. Next trade will use correct runner system.
This commit is contained in:
227
.github/copilot-instructions.md
vendored
227
.github/copilot-instructions.md
vendored
@@ -19,7 +19,7 @@
|
||||
- BTC and other symbols fall back to global settings (`MAX_POSITION_SIZE_USD`, `LEVERAGE`)
|
||||
- **Priority:** Per-symbol ENV → Market config → Global ENV → Defaults
|
||||
|
||||
**Signal Quality System:** Filters trades based on 5 metrics (ATR, ADX, RSI, volumeRatio, pricePosition) scored 0-100. Minimum score threshold configurable via `MIN_SIGNAL_QUALITY_SCORE` env var (default: 65, editable via settings page). Scores stored in database for future optimization.
|
||||
**Signal Quality System:** Filters trades based on 5 metrics (ATR, ADX, RSI, volumeRatio, pricePosition) scored 0-100. Only trades scoring 60+ are executed. Scores stored in database for future optimization.
|
||||
|
||||
**Timeframe-Aware Scoring:** Signal quality thresholds adjust based on timeframe (5min vs daily):
|
||||
- 5min: ADX 12+ trending (vs 18+ for daily), ATR 0.2-0.7% healthy (vs 0.4%+ for daily)
|
||||
@@ -30,30 +30,12 @@
|
||||
|
||||
**Manual Trading via Telegram:** Send plain-text messages like `long sol`, `short eth`, `long btc` to open positions instantly (bypasses n8n, calls `/api/trading/execute` directly with preset healthy metrics).
|
||||
|
||||
## Recent Critical Fixes (2024-11-10)
|
||||
|
||||
### Runner System - Three Cascading Bugs Fixed
|
||||
The TP2-as-runner feature was broken by three separate bugs:
|
||||
|
||||
1. **P&L Calculation Bug (65x inflation)** - `lib/drift/orders.ts`, `lib/trading/position-manager.ts`
|
||||
- Calculated P&L on notional ($2,100) instead of collateral ($210)
|
||||
- Database showed +$1,345, reality was -$806 loss
|
||||
- Fix: `collateralUSD = notional / leverage`, calculate P&L on collateral
|
||||
|
||||
2. **Post-TP1 Logic Bug** - `lib/trading/position-manager.ts` lines 1010-1030
|
||||
- Placed TP order at TP2 price after TP1 hit (closed position instead of trailing)
|
||||
- Fix: Check `if (config.takeProfit2SizePercent === 0)` to skip TP orders
|
||||
|
||||
3. **JavaScript || Operator Bug** - `app/api/trading/execute/route.ts`, `test/route.ts`
|
||||
- `config.takeProfit2SizePercent || 100` treated 0 as falsy → returned 100
|
||||
- Fix: Use `??` (nullish coalescing) instead of `||` for numeric defaults
|
||||
|
||||
### Anti-Chop Filter V2 - Range-Bound Detection
|
||||
- **Problem:** Flip-flop trades in sideways markets (stopped out in 8-24 seconds)
|
||||
- **Fix:** -25 points when price position <40% AND ADX <25 (both conditions)
|
||||
- **Location:** `lib/trading/signal-quality.ts` lines 145-165
|
||||
- **Impact:** Win rate 43.8% → 55.6%, profit per trade +86%
|
||||
- **Backtest:** Would have blocked all 3 flip-flop trades from today
|
||||
**Re-Entry Analytics System:** Manual trades are validated before execution using fresh TradingView data:
|
||||
- Market data cached from TradingView signals (5min expiry)
|
||||
- `/api/analytics/reentry-check` scores re-entry based on fresh metrics + recent performance
|
||||
- Telegram bot blocks low-quality re-entries unless `--force` flag used
|
||||
- Uses real TradingView ADX/ATR/RSI when available, falls back to historical data
|
||||
- Penalty for recent losing trades, bonus for winning streaks
|
||||
|
||||
## Critical Components
|
||||
|
||||
@@ -76,14 +58,11 @@ scoreSignalQuality({
|
||||
**Price position penalties (all timeframes):**
|
||||
- Long at 90-95%+ range: -15 to -30 points (chasing highs)
|
||||
- Short at <5-10% range: -15 to -30 points (chasing lows)
|
||||
- **ANTI-CHOP (v2024-11-10):** Price position <40% + ADX <25 = -25 points (RANGE-BOUND CHOP)
|
||||
- Prevents flip-flop losses from entering range extremes
|
||||
- Targets sideways markets where price is low in range but trend is weak
|
||||
- Backtest: 43.8% → 55.6% win rate, 86% higher profit per trade
|
||||
- Prevents flip-flop losses from entering range extremes
|
||||
|
||||
**Key behaviors:**
|
||||
- Returns score 0-100 and detailed breakdown object
|
||||
- Minimum score threshold configurable via `config.minSignalQualityScore` (default: 65)
|
||||
- Minimum score 60 required to execute trade
|
||||
- Called by both `/api/trading/check-risk` and `/api/trading/execute`
|
||||
- Scores saved to database for post-trade analysis
|
||||
|
||||
@@ -115,18 +94,23 @@ await positionManager.addTrade(activeTrade)
|
||||
**Manual trade commands via plain text:**
|
||||
```python
|
||||
# User sends plain text message (not slash commands)
|
||||
"long sol" → Opens SOL-PERP long position
|
||||
"short eth" → Opens ETH-PERP short position
|
||||
"long btc" → Opens BTC-PERP long position
|
||||
"long sol" → Validates via analytics, then opens SOL-PERP long
|
||||
"short eth" → Validates via analytics, then opens ETH-PERP short
|
||||
"long btc --force" → Skips analytics validation, opens BTC-PERP long immediately
|
||||
```
|
||||
|
||||
**Key behaviors:**
|
||||
- MessageHandler processes all text messages (not just commands)
|
||||
- Maps user-friendly symbols (sol, eth, btc) to Drift format (SOL-PERP, etc.)
|
||||
- Calls `/api/trading/execute` directly with preset healthy metrics (ATR=1.0, ADX=25, RSI=50, volumeRatio=1.2)
|
||||
- **Analytics validation:** Calls `/api/analytics/reentry-check` before execution
|
||||
- Blocks trades with score <55 unless `--force` flag used
|
||||
- Uses fresh TradingView data (<5min old) when available
|
||||
- Falls back to historical metrics with penalty
|
||||
- Considers recent trade performance (last 3 trades)
|
||||
- Calls `/api/trading/execute` directly with preset healthy metrics (ATR=0.45, ADX=32, RSI=58/42)
|
||||
- Bypasses n8n workflow and TradingView requirements
|
||||
- 60-second timeout for API calls
|
||||
- Responds with trade confirmation or error message
|
||||
- Responds with trade confirmation or analytics rejection message
|
||||
|
||||
**Status command:**
|
||||
```python
|
||||
@@ -152,6 +136,7 @@ const health = await driftService.getAccountHealth()
|
||||
- `openPosition()` - Opens market position with transaction confirmation
|
||||
- `closePosition()` - Closes position with transaction confirmation
|
||||
- `placeExitOrders()` - Places TP/SL orders on-chain
|
||||
- `cancelAllOrders()` - Cancels all reduce-only orders for a market
|
||||
|
||||
**CRITICAL: Transaction Confirmation Pattern**
|
||||
Both `openPosition()` and `closePosition()` MUST confirm transactions on-chain:
|
||||
@@ -168,6 +153,46 @@ console.log('✅ Transaction confirmed on-chain')
|
||||
```
|
||||
Without this, the SDK returns signatures for transactions that never execute, causing phantom trades/closes.
|
||||
|
||||
**CRITICAL: Drift SDK position.size is USD, not tokens**
|
||||
The Drift SDK returns `position.size` as USD notional value, NOT token quantity:
|
||||
```typescript
|
||||
// WRONG: Multiply by price (inflates by 156x for SOL at $157)
|
||||
const positionSizeUSD = position.size * currentPrice
|
||||
|
||||
// CORRECT: Use directly as USD value
|
||||
const positionSizeUSD = Math.abs(position.size)
|
||||
```
|
||||
This affects Position Manager's TP1 detection - if calculated incorrectly, TP1 will never trigger because expected size won't match actual size.
|
||||
|
||||
**Solana RPC Rate Limiting with Exponential Backoff**
|
||||
Solana RPC endpoints return 429 errors under load. Always use retry logic for order operations:
|
||||
```typescript
|
||||
export async function retryWithBackoff<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
initialDelay: number = 2000
|
||||
): Promise<T> {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation()
|
||||
} catch (error: any) {
|
||||
if (error?.message?.includes('429') && attempt < maxRetries - 1) {
|
||||
const delay = initialDelay * Math.pow(2, attempt)
|
||||
console.log(`⏳ Rate limited, retrying in ${delay/1000}s... (attempt ${attempt + 1}/${maxRetries})`)
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
throw new Error('Max retries exceeded')
|
||||
}
|
||||
|
||||
// Usage in cancelAllOrders
|
||||
await retryWithBackoff(() => driftClient.cancelOrders(...))
|
||||
```
|
||||
Without this, order cancellations fail silently during TP1→breakeven order updates, leaving ghost orders that cause incorrect fills.
|
||||
|
||||
**Dual Stop System** (USE_DUAL_STOPS=true):
|
||||
```typescript
|
||||
// Soft stop: TRIGGER_LIMIT at -1.5% (avoids wicks)
|
||||
@@ -246,14 +271,16 @@ const driftSymbol = normalizeTradingViewSymbol(body.symbol)
|
||||
7. Add to Position Manager if applicable
|
||||
|
||||
**Key endpoints:**
|
||||
- `/api/trading/execute` - Main entry point from n8n (production, requires auth)
|
||||
- `/api/trading/execute` - Main entry point from n8n (production, requires auth), **auto-caches market data**
|
||||
- `/api/trading/check-risk` - Pre-execution validation (duplicate check, quality score, **per-symbol cooldown**, rate limits, **symbol enabled check**)
|
||||
- `/api/trading/test` - Test trades from settings UI (no auth required, **respects symbol enable/disable**)
|
||||
- `/api/trading/close` - Manual position closing
|
||||
- `/api/trading/close` - Manual position closing (requires symbol normalization)
|
||||
- `/api/trading/cancel-orders` - **Manual order cleanup** (for stuck/ghost orders after rate limit failures)
|
||||
- `/api/trading/positions` - Query open positions from Drift
|
||||
- `/api/trading/sync-positions` - **Re-sync Position Manager with actual Drift positions** (no auth, for recovery from partial fills/restarts)
|
||||
- `/api/trading/market-data` - Webhook for TradingView market data updates (GET for debug, POST for data)
|
||||
- `/api/settings` - Get/update config (writes to .env file, **includes per-symbol settings**)
|
||||
- `/api/analytics/last-trade` - Fetch most recent trade details for dashboard (includes quality score)
|
||||
- `/api/analytics/reentry-check` - **Validate manual re-entry** with fresh TradingView data + recent performance
|
||||
- `/api/analytics/version-comparison` - Compare performance across signal quality logic versions (v1/v2/v3)
|
||||
- `/api/restart` - Create restart flag for watch-restart.sh script
|
||||
|
||||
@@ -401,102 +428,68 @@ docker exec trading-bot-postgres psql -U postgres -d trading_bot_v4 -c "\dt"
|
||||
|
||||
6. **Type errors with Prisma:** The Trade type from Prisma is only available AFTER `npx prisma generate` - use explicit types or `// @ts-ignore` carefully
|
||||
|
||||
7. **Hardcoded config values:** NEVER use hardcoded values for configurable settings in API endpoints. Always read from `config.minSignalQualityScore` or similar config properties. Settings changed via the UI won't take effect if endpoints use hardcoded values.
|
||||
7. **Quality score duplication:** Signal quality calculation exists in BOTH `check-risk` and `execute` endpoints - keep logic synchronized
|
||||
|
||||
8. **Quality score duplication:** Signal quality calculation exists in BOTH `check-risk` and `execute` endpoints - keep logic synchronized
|
||||
|
||||
9. **TP2-as-Runner configuration:**
|
||||
8. **TP2-as-Runner configuration:**
|
||||
- `takeProfit2SizePercent: 0` means "TP2 activates trailing stop, no position close"
|
||||
- This creates 25% runner (vs old 5% system) for better profit capture
|
||||
- `TAKE_PROFIT_2_PERCENT=0.7` sets TP2 trigger price, `TAKE_PROFIT_2_SIZE_PERCENT` should be 0
|
||||
- Settings UI correctly shows "TP2 activates trailing stop" instead of size percentage
|
||||
|
||||
10. **P&L calculation CRITICAL:** Use actual entry vs exit price calculation, not SDK values:
|
||||
9. **P&L calculation CRITICAL:** Use actual entry vs exit price calculation, not SDK values:
|
||||
```typescript
|
||||
const profitPercent = this.calculateProfitPercent(trade.entryPrice, exitPrice, trade.direction)
|
||||
const actualRealizedPnL = (closedSizeUSD * profitPercent) / 100
|
||||
trade.realizedPnL += actualRealizedPnL // NOT: result.realizedPnL from SDK
|
||||
```
|
||||
|
||||
11. **Transaction confirmation CRITICAL:** Both `openPosition()` AND `closePosition()` MUST call `connection.confirmTransaction()` after `placePerpOrder()`. Without this, the SDK returns transaction signatures that aren't confirmed on-chain, causing "phantom trades" or "phantom closes". Always check `confirmation.value.err` before proceeding.
|
||||
10. **Transaction confirmation CRITICAL:** Both `openPosition()` AND `closePosition()` MUST call `connection.confirmTransaction()` after `placePerpOrder()`. Without this, the SDK returns transaction signatures that aren't confirmed on-chain, causing "phantom trades" or "phantom closes". Always check `confirmation.value.err` before proceeding.
|
||||
|
||||
12. **Execution order matters:** When creating trades via API endpoints, the order MUST be:
|
||||
11. **Execution order matters:** When creating trades via API endpoints, the order MUST be:
|
||||
1. Open position + place exit orders
|
||||
2. Save to database (`createTrade()`)
|
||||
3. Add to Position Manager (`positionManager.addTrade()`)
|
||||
|
||||
If Position Manager is added before database save, race conditions occur where monitoring checks before the trade exists in DB.
|
||||
|
||||
13. **New trade grace period:** Position Manager skips "external closure" detection for trades <30 seconds old because Drift positions take 5-10 seconds to propagate after opening. Without this grace period, new positions are immediately detected as "closed externally" and cancelled.
|
||||
12. **New trade grace period:** Position Manager skips "external closure" detection for trades <30 seconds old because Drift positions take 5-10 seconds to propagate after opening. Without this grace period, new positions are immediately detected as "closed externally" and cancelled.
|
||||
|
||||
14. **Drift minimum position sizes:** Actual minimums differ from documentation:
|
||||
13. **Drift minimum position sizes:** Actual minimums differ from documentation:
|
||||
- SOL-PERP: 0.1 SOL (~$5-15 depending on price)
|
||||
- ETH-PERP: 0.01 ETH (~$38-40 at $4000/ETH)
|
||||
- BTC-PERP: 0.0001 BTC (~$10-12 at $100k/BTC)
|
||||
|
||||
Always calculate: `minOrderSize × currentPrice` must exceed Drift's $4 minimum. Add buffer for price movement.
|
||||
|
||||
15. **Exit reason detection bug:** Position Manager was using current price to determine exit reason, but on-chain orders filled at a DIFFERENT price in the past. Now uses `trade.tp1Hit` / `trade.tp2Hit` flags and realized P&L to correctly identify whether TP1, TP2, or SL triggered. Prevents profitable trades being mislabeled as "SL" exits.
|
||||
14. **Exit reason detection bug:** Position Manager was using current price to determine exit reason, but on-chain orders filled at a DIFFERENT price in the past. Now uses `trade.tp1Hit` / `trade.tp2Hit` flags and realized P&L to correctly identify whether TP1, TP2, or SL triggered. Prevents profitable trades being mislabeled as "SL" exits.
|
||||
|
||||
16. **Per-symbol cooldown:** Cooldown period is per-symbol, NOT global. ETH trade at 10:00 does NOT block SOL trade at 10:01. Each coin (SOL/ETH/BTC) has independent cooldown timer to avoid missing opportunities on different assets.
|
||||
15. **Per-symbol cooldown:** Cooldown period is per-symbol, NOT global. ETH trade at 10:00 does NOT block SOL trade at 10:01. Each coin (SOL/ETH/BTC) has independent cooldown timer to avoid missing opportunities on different assets.
|
||||
|
||||
17. **Timeframe-aware scoring crucial:** Signal quality thresholds MUST adjust for 5min vs higher timeframes:
|
||||
16. **Timeframe-aware scoring crucial:** Signal quality thresholds MUST adjust for 5min vs higher timeframes:
|
||||
- 5min charts naturally have lower ADX (12-22 healthy) and ATR (0.2-0.7% healthy) than daily charts
|
||||
- Without timeframe awareness, valid 5min breakouts get blocked as "low quality"
|
||||
- Anti-chop filter applies -20 points for extreme sideways regardless of timeframe
|
||||
- Always pass `timeframe` parameter from TradingView alerts to `scoreSignalQuality()`
|
||||
|
||||
18. **Price position chasing causes flip-flops:** Opening longs at 90%+ range or shorts at <10% range reliably loses money:
|
||||
17. **Price position chasing causes flip-flops:** Opening longs at 90%+ range or shorts at <10% range reliably loses money:
|
||||
- Database analysis showed overnight flip-flop losses all had price position 9-94% (chasing extremes)
|
||||
- These trades had valid ADX (16-18) but entered at worst possible time
|
||||
- Quality scoring now penalizes -15 to -30 points for range extremes
|
||||
- Prevents rapid reversals when price is already overextended
|
||||
|
||||
19. **TradingView ADX minimum for 5min:** Set ADX filter to 15 (not 20+) in TradingView alerts for 5min charts:
|
||||
18. **TradingView ADX minimum for 5min:** Set ADX filter to 15 (not 20+) in TradingView alerts for 5min charts:
|
||||
- Higher timeframes can use ADX 20+ for strong trends
|
||||
- 5min charts need lower threshold to catch valid breakouts
|
||||
- Bot's quality scoring provides second-layer filtering with context-aware metrics
|
||||
- Two-stage filtering (TradingView + bot) prevents both overtrading and missing valid signals
|
||||
|
||||
20. **Prisma Decimal type handling:** Raw SQL queries return Prisma `Decimal` objects, not plain numbers:
|
||||
19. **Prisma Decimal type handling:** Raw SQL queries return Prisma `Decimal` objects, not plain numbers:
|
||||
- Use `any` type for numeric fields in `$queryRaw` results: `total_pnl: any`
|
||||
- Convert with `Number()` before returning to frontend: `totalPnL: Number(stat.total_pnl) || 0`
|
||||
- Frontend uses `.toFixed()` which doesn't exist on Decimal objects
|
||||
- Applies to all aggregations: SUM(), AVG(), ROUND() - all return Decimal types
|
||||
- Example: `/api/analytics/version-comparison` converts all numeric fields
|
||||
|
||||
21. **JavaScript || vs ?? operators CRITICAL:** When setting default values for numeric config, ALWAYS use `??` (nullish coalescing):
|
||||
```typescript
|
||||
// WRONG - treats 0 as falsy:
|
||||
tp2SizePercent: config.takeProfit2SizePercent || 100 // 0 becomes 100!
|
||||
|
||||
// CORRECT - only null/undefined are nullish:
|
||||
tp2SizePercent: config.takeProfit2SizePercent ?? 100 // 0 stays 0
|
||||
```
|
||||
- `||` treats `0`, `false`, `""`, `null`, `undefined` as falsy
|
||||
- `??` only treats `null` and `undefined` as nullish
|
||||
- Critical for runner system: `TAKE_PROFIT_2_SIZE_PERCENT=0` must be respected
|
||||
- This bug caused TP2 orders to be placed at 100% despite config setting 0
|
||||
- Applies to ALL numeric config values where 0 is valid (TP sizes, leverage, thresholds)
|
||||
|
||||
22. **Range-bound chop detection:** The anti-chop filter V2 (implemented 2024-11-10) prevents flip-flop losses:
|
||||
- Detection: Price position <40% of range + ADX <25 = weak range-bound market
|
||||
- Penalty: -25 points to signal quality score
|
||||
- Why: Trades entering early in range with weak trend get whipsawed in seconds
|
||||
- Evidence: Backtest showed 5 flip-flop trades (8-24 second holds) all had this pattern
|
||||
- Result: Win rate improved from 43.8% to 55.6%, profit per trade +86%
|
||||
- Implementation: `lib/trading/signal-quality.ts` checks both conditions before price position scoring
|
||||
|
||||
23. **Position Manager sync issues:** Partial fills from on-chain orders can cause Position Manager to lose tracking:
|
||||
- Symptom: Database shows position "closed", but Drift shows position still open without stop loss
|
||||
- Cause: On-chain orders partially fill (0.29 SOL × 3 times), Position Manager closes database record, but remainder stays open
|
||||
- Impact: Remaining position has NO software-based stop loss protection (only relies on on-chain orders)
|
||||
- Solution: Use `/api/trading/sync-positions` endpoint to re-sync Position Manager with actual Drift positions
|
||||
- Access: Settings UI "Sync Positions" button (orange), or CLI `scripts/sync-positions.sh`
|
||||
- When: After manual Telegram trades, bot restarts, rate limiting issues, or suspected tracking loss
|
||||
- Recovery: Endpoint fetches actual Drift positions, re-adds missing ones to Position Manager with calculated TP/SL
|
||||
- Documentation: See `docs/guides/POSITION_SYNC_GUIDE.md` for details
|
||||
|
||||
## File Conventions
|
||||
|
||||
- **API routes:** `app/api/[feature]/[action]/route.ts` (Next.js 15 App Router)
|
||||
@@ -505,6 +498,70 @@ trade.realizedPnL += actualRealizedPnL // NOT: result.realizedPnL from SDK
|
||||
- **Types:** Define interfaces in same file as implementation (not separate types directory)
|
||||
- **Console logs:** Use emojis for visual scanning: 🎯 🚀 ✅ ❌ 💰 📊 🛡️
|
||||
|
||||
## Re-Entry Analytics System (Phase 1)
|
||||
|
||||
**Purpose:** Validate manual Telegram trades using fresh TradingView data + recent performance analysis
|
||||
|
||||
**Components:**
|
||||
1. **Market Data Cache** (`lib/trading/market-data-cache.ts`)
|
||||
- Singleton service storing TradingView metrics
|
||||
- 5-minute expiry on cached data
|
||||
- Tracks: ATR, ADX, RSI, volume ratio, price position, timeframe
|
||||
|
||||
2. **Market Data Webhook** (`app/api/trading/market-data/route.ts`)
|
||||
- Receives TradingView alerts every 1-5 minutes
|
||||
- POST: Updates cache with fresh metrics
|
||||
- GET: View cached data (debugging)
|
||||
|
||||
3. **Re-Entry Check Endpoint** (`app/api/analytics/reentry-check/route.ts`)
|
||||
- Validates manual trade requests
|
||||
- Uses fresh TradingView data if available (<5min old)
|
||||
- Falls back to historical metrics from last trade
|
||||
- Scores signal quality + applies performance modifiers:
|
||||
- **-20 points** if last 3 trades lost money (avgPnL < -5%)
|
||||
- **+10 points** if last 3 trades won (avgPnL > +5%, WR >= 66%)
|
||||
- **-5 points** for stale data, **-10 points** for no data
|
||||
- Minimum score: 55 (vs 60 for new signals)
|
||||
|
||||
4. **Auto-Caching** (`app/api/trading/execute/route.ts`)
|
||||
- Every trade signal from TradingView auto-caches metrics
|
||||
- Ensures fresh data available for manual re-entries
|
||||
|
||||
5. **Telegram Integration** (`telegram_command_bot.py`)
|
||||
- Calls `/api/analytics/reentry-check` before executing manual trades
|
||||
- Shows data freshness ("✅ FRESH 23s old" vs "⚠️ Historical")
|
||||
- Blocks low-quality re-entries unless `--force` flag used
|
||||
- Fail-open: Proceeds if analytics check fails
|
||||
|
||||
**User Flow:**
|
||||
```
|
||||
User: "long sol"
|
||||
↓ Check cache for SOL-PERP
|
||||
↓ Fresh data? → Use real TradingView metrics
|
||||
↓ Stale/missing? → Use historical + penalty
|
||||
↓ Score quality + recent performance
|
||||
↓ Score >= 55? → Execute
|
||||
↓ Score < 55? → Block (unless --force)
|
||||
```
|
||||
|
||||
**TradingView Setup:**
|
||||
Create alerts that fire every 1-5 minutes with this webhook 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: `https://your-domain.com/api/trading/market-data`
|
||||
|
||||
## Per-Symbol Trading Controls
|
||||
|
||||
**Purpose:** Independent enable/disable toggles and position sizing for SOL and ETH to support different trading strategies (e.g., ETH for data collection at minimal size, SOL for profit generation).
|
||||
|
||||
@@ -264,17 +264,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
const hasContextMetrics = body.atr !== undefined && body.atr > 0
|
||||
|
||||
if (hasContextMetrics) {
|
||||
console.log('🔍 Risk check for:', {
|
||||
symbol: body.symbol,
|
||||
direction: body.direction,
|
||||
timeframe: body.timeframe, // DEBUG: Check if timeframe is received
|
||||
atr: body.atr,
|
||||
adx: body.adx,
|
||||
rsi: body.rsi,
|
||||
volumeRatio: body.volumeRatio,
|
||||
pricePosition: body.pricePosition
|
||||
})
|
||||
|
||||
const qualityScore = scoreSignalQuality({
|
||||
atr: body.atr || 0,
|
||||
adx: body.adx || 0,
|
||||
@@ -283,7 +272,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
pricePosition: body.pricePosition || 0,
|
||||
direction: body.direction,
|
||||
timeframe: body.timeframe, // Pass timeframe for context-aware scoring
|
||||
minScore: config.minSignalQualityScore // Use config value (editable via settings page)
|
||||
minScore: 60 // Hardcoded threshold
|
||||
})
|
||||
|
||||
if (!qualityScore.passed) {
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { initializeDriftService } from '@/lib/drift/client'
|
||||
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
||||
import { normalizeTradingViewSymbol, calculateDynamicTp2 } from '@/config/trading'
|
||||
import { normalizeTradingViewSymbol } from '@/config/trading'
|
||||
import { getMergedConfig } from '@/config/trading'
|
||||
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||
import { createTrade, updateTradeExit } from '@/lib/database/trades'
|
||||
import { scoreSignalQuality } from '@/lib/trading/signal-quality'
|
||||
import { getMarketDataCache } from '@/lib/trading/market-data-cache'
|
||||
|
||||
export interface ExecuteTradeRequest {
|
||||
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
||||
@@ -35,8 +36,6 @@ export interface ExecuteTradeResponse {
|
||||
direction?: 'long' | 'short'
|
||||
entryPrice?: number
|
||||
positionSize?: number
|
||||
requestedPositionSize?: number
|
||||
fillCoveragePercent?: number
|
||||
leverage?: number
|
||||
stopLoss?: number
|
||||
takeProfit1?: number
|
||||
@@ -88,34 +87,29 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
const driftSymbol = normalizeTradingViewSymbol(body.symbol)
|
||||
console.log(`📊 Normalized symbol: ${body.symbol} → ${driftSymbol}`)
|
||||
|
||||
// 🆕 Cache incoming market data from TradingView signals
|
||||
if (body.atr && body.adx && body.rsi) {
|
||||
const marketCache = getMarketDataCache()
|
||||
marketCache.set(driftSymbol, {
|
||||
symbol: driftSymbol,
|
||||
atr: body.atr,
|
||||
adx: body.adx,
|
||||
rsi: body.rsi,
|
||||
volumeRatio: body.volumeRatio || 1.0,
|
||||
pricePosition: body.pricePosition || 50,
|
||||
currentPrice: body.signalPrice || 0,
|
||||
timestamp: Date.now(),
|
||||
timeframe: body.timeframe || '5'
|
||||
})
|
||||
console.log(`📊 Market data auto-cached for ${driftSymbol} from trade signal`)
|
||||
}
|
||||
|
||||
// Get trading configuration
|
||||
const config = getMergedConfig()
|
||||
|
||||
// Initialize Drift service to get account balance
|
||||
const driftService = await initializeDriftService()
|
||||
|
||||
// Check account health before trading
|
||||
const health = await driftService.getAccountHealth()
|
||||
console.log('💊 Account health:', health)
|
||||
|
||||
if (health.freeCollateral <= 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Insufficient collateral',
|
||||
message: `Free collateral: $${health.freeCollateral.toFixed(2)}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get symbol-specific position sizing (with percentage support)
|
||||
const { getActualPositionSizeForSymbol } = await import('@/config/trading')
|
||||
const { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol(
|
||||
driftSymbol,
|
||||
config,
|
||||
health.freeCollateral
|
||||
)
|
||||
// Get symbol-specific position sizing
|
||||
const { getPositionSizeForSymbol } = await import('@/config/trading')
|
||||
const { size: positionSize, leverage, enabled } = getPositionSizeForSymbol(driftSymbol, config)
|
||||
|
||||
// Check if trading is enabled for this symbol
|
||||
if (!enabled) {
|
||||
@@ -134,9 +128,25 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
console.log(` Enabled: ${enabled}`)
|
||||
console.log(` Position size: $${positionSize}`)
|
||||
console.log(` Leverage: ${leverage}x`)
|
||||
console.log(` Using percentage: ${usePercentage}`)
|
||||
console.log(` Free collateral: $${health.freeCollateral.toFixed(2)}`)
|
||||
|
||||
// Initialize Drift service if not already initialized
|
||||
const driftService = await initializeDriftService()
|
||||
|
||||
// Check account health before trading
|
||||
const health = await driftService.getAccountHealth()
|
||||
console.log('💊 Account health:', health)
|
||||
|
||||
if (health.freeCollateral <= 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Insufficient collateral',
|
||||
message: `Free collateral: $${health.freeCollateral.toFixed(2)}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// AUTO-FLIP: Check for existing opposite direction position
|
||||
const positionManager = await getInitializedPositionManager()
|
||||
const existingTrades = Array.from(positionManager.getActiveTrades().values())
|
||||
@@ -186,16 +196,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
|
||||
// Update Position Manager tracking
|
||||
const timesScaled = (sameDirectionPosition.timesScaled || 0) + 1
|
||||
const actualScaleNotional = scaleResult.actualSizeUSD ?? scaleSize
|
||||
const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + actualScaleNotional
|
||||
const newTotalSize = sameDirectionPosition.currentSize + actualScaleNotional
|
||||
|
||||
if (scaleSize > 0) {
|
||||
const coverage = (actualScaleNotional / scaleSize) * 100
|
||||
if (coverage < 99.5) {
|
||||
console.log(`⚠️ Scale fill coverage: ${coverage.toFixed(2)}% of requested $${scaleSize.toFixed(2)}`)
|
||||
}
|
||||
}
|
||||
const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + scaleSize
|
||||
const newTotalSize = sameDirectionPosition.currentSize + (scaleResult.fillSize || 0)
|
||||
|
||||
// Update the trade tracking (simplified - just update the active trade object)
|
||||
sameDirectionPosition.timesScaled = timesScaled
|
||||
@@ -285,20 +287,20 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
}
|
||||
|
||||
// Calculate requested position size with leverage
|
||||
const requestedPositionSizeUSD = positionSize * leverage
|
||||
// Calculate position size with leverage
|
||||
const positionSizeUSD = positionSize * leverage
|
||||
|
||||
console.log(`💰 Opening ${body.direction} position:`)
|
||||
console.log(` Symbol: ${driftSymbol}`)
|
||||
console.log(` Base size: $${positionSize}`)
|
||||
console.log(` Leverage: ${leverage}x`)
|
||||
console.log(` Requested notional: $${requestedPositionSizeUSD}`)
|
||||
console.log(` Total position: $${positionSizeUSD}`)
|
||||
|
||||
// Open position
|
||||
const openResult = await openPosition({
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
sizeUSD: requestedPositionSizeUSD,
|
||||
sizeUSD: positionSizeUSD,
|
||||
slippageTolerance: config.slippageTolerance,
|
||||
})
|
||||
|
||||
@@ -316,7 +318,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
// CRITICAL: Check for phantom trade (position opened but size mismatch)
|
||||
if (openResult.isPhantom) {
|
||||
console.error(`🚨 PHANTOM TRADE DETECTED - Not adding to Position Manager`)
|
||||
console.error(` Expected: $${requestedPositionSizeUSD.toFixed(2)}`)
|
||||
console.error(` Expected: $${positionSizeUSD.toFixed(2)}`)
|
||||
console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`)
|
||||
|
||||
// Save phantom trade to database for analysis
|
||||
@@ -336,7 +338,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
entryPrice: openResult.fillPrice!,
|
||||
positionSizeUSD: requestedPositionSizeUSD,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice: 0, // Not applicable for phantom
|
||||
takeProfit1Price: 0,
|
||||
@@ -356,7 +358,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
// Phantom-specific fields
|
||||
status: 'phantom',
|
||||
isPhantom: true,
|
||||
expectedSizeUSD: requestedPositionSizeUSD,
|
||||
expectedSizeUSD: positionSizeUSD,
|
||||
actualSizeUSD: openResult.actualSizeUSD,
|
||||
phantomReason: 'ORACLE_PRICE_MISMATCH', // Likely cause based on logs
|
||||
})
|
||||
@@ -370,7 +372,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
{
|
||||
success: false,
|
||||
error: 'Phantom trade detected',
|
||||
message: `Position opened but size mismatch detected. Expected $${requestedPositionSizeUSD.toFixed(2)}, got $${openResult.actualSizeUSD?.toFixed(2)}. This usually indicates oracle price was stale or order was rejected by exchange.`,
|
||||
message: `Position opened but size mismatch detected. Expected $${positionSizeUSD.toFixed(2)}, got $${openResult.actualSizeUSD?.toFixed(2)}. This usually indicates oracle price was stale or order was rejected by exchange.`,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
@@ -378,20 +380,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
|
||||
// Calculate stop loss and take profit prices
|
||||
const entryPrice = openResult.fillPrice!
|
||||
const actualPositionSizeUSD = openResult.actualSizeUSD ?? requestedPositionSizeUSD
|
||||
const filledBaseSize = openResult.fillSize !== undefined
|
||||
? Math.abs(openResult.fillSize)
|
||||
: (entryPrice > 0 ? actualPositionSizeUSD / entryPrice : 0)
|
||||
const fillCoverage = requestedPositionSizeUSD > 0
|
||||
? (actualPositionSizeUSD / requestedPositionSizeUSD) * 100
|
||||
: 100
|
||||
|
||||
console.log('📏 Fill results:')
|
||||
console.log(` Filled base size: ${filledBaseSize.toFixed(4)} ${driftSymbol.split('-')[0]}`)
|
||||
console.log(` Filled notional: $${actualPositionSizeUSD.toFixed(2)}`)
|
||||
if (fillCoverage < 99.5) {
|
||||
console.log(` ⚠️ Partial fill: ${fillCoverage.toFixed(2)}% of requested size`)
|
||||
}
|
||||
|
||||
const stopLossPrice = calculatePrice(
|
||||
entryPrice,
|
||||
@@ -425,15 +413,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
body.direction
|
||||
)
|
||||
|
||||
const dynamicTp2Percent = calculateDynamicTp2(
|
||||
entryPrice,
|
||||
body.atr || 0, // ATR from TradingView signal
|
||||
config
|
||||
)
|
||||
|
||||
const tp2Price = calculatePrice(
|
||||
entryPrice,
|
||||
dynamicTp2Percent,
|
||||
config.takeProfit2Percent,
|
||||
body.direction
|
||||
)
|
||||
|
||||
@@ -441,7 +423,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
console.log(` Entry: $${entryPrice.toFixed(4)}`)
|
||||
console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`)
|
||||
console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`)
|
||||
console.log(` TP2: $${tp2Price.toFixed(4)} (${dynamicTp2Percent.toFixed(2)}% - ATR-based)`)
|
||||
console.log(` TP2: $${tp2Price.toFixed(4)} (${config.takeProfit2Percent}%)`)
|
||||
|
||||
// Calculate emergency stop
|
||||
const emergencyStopPrice = calculatePrice(
|
||||
@@ -458,13 +440,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
direction: body.direction,
|
||||
entryPrice,
|
||||
entryTime: Date.now(),
|
||||
positionSize: actualPositionSizeUSD,
|
||||
positionSize: positionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice,
|
||||
tp1Price,
|
||||
tp2Price,
|
||||
emergencyStopPrice,
|
||||
currentSize: actualPositionSizeUSD,
|
||||
currentSize: positionSizeUSD,
|
||||
tp1Hit: false,
|
||||
tp2Hit: false,
|
||||
slMovedToBreakeven: false,
|
||||
@@ -483,8 +465,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
originalAdx: body.adx, // Store for scaling validation
|
||||
timesScaled: 0,
|
||||
totalScaleAdded: 0,
|
||||
atrAtEntry: body.atr,
|
||||
runnerTrailingPercent: undefined,
|
||||
priceCheckCount: 0,
|
||||
lastPrice: entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
@@ -497,13 +477,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
try {
|
||||
const exitRes = await placeExitOrders({
|
||||
symbol: driftSymbol,
|
||||
positionSizeUSD: actualPositionSizeUSD,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
entryPrice: entryPrice,
|
||||
tp1Price,
|
||||
tp2Price,
|
||||
stopLossPrice,
|
||||
tp1SizePercent: config.takeProfit1SizePercent ?? 50,
|
||||
tp2SizePercent: config.takeProfit2SizePercent ?? 100, // Use ?? instead of || to allow 0
|
||||
tp1SizePercent: config.takeProfit1SizePercent ?? 75,
|
||||
tp2SizePercent: config.takeProfit2SizePercent ?? 0, // 0 = activate trailing stop, don't close
|
||||
direction: body.direction,
|
||||
// Dual stop parameters
|
||||
useDualStops: config.useDualStops,
|
||||
@@ -534,16 +514,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
entryPrice: entryPrice,
|
||||
positionSize: actualPositionSizeUSD,
|
||||
requestedPositionSize: requestedPositionSizeUSD,
|
||||
fillCoveragePercent: Number(fillCoverage.toFixed(2)),
|
||||
positionSize: positionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLoss: stopLossPrice,
|
||||
takeProfit1: tp1Price,
|
||||
takeProfit2: tp2Price,
|
||||
stopLossPercent: config.stopLossPercent,
|
||||
tp1Percent: config.takeProfit1Percent,
|
||||
tp2Percent: dynamicTp2Percent,
|
||||
tp2Percent: config.takeProfit2Percent,
|
||||
entrySlippage: openResult.slippage,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
@@ -571,13 +549,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
entryPrice,
|
||||
positionSizeUSD: actualPositionSizeUSD,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice,
|
||||
takeProfit1Price: tp1Price,
|
||||
takeProfit2Price: tp2Price,
|
||||
tp1SizePercent: config.takeProfit1SizePercent || 50,
|
||||
tp2SizePercent: config.takeProfit2SizePercent || 100,
|
||||
tp1SizePercent: config.takeProfit1SizePercent ?? 75,
|
||||
tp2SizePercent: config.takeProfit2SizePercent ?? 0, // Use ?? to allow 0 for runner system
|
||||
configSnapshot: config,
|
||||
entryOrderTx: openResult.transactionSignature!,
|
||||
tp1OrderTx: exitOrderSignatures[0],
|
||||
@@ -596,8 +574,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
volumeAtEntry: body.volumeRatio,
|
||||
pricePositionAtEntry: body.pricePosition,
|
||||
signalQualityScore: qualityResult.score,
|
||||
expectedSizeUSD: requestedPositionSizeUSD,
|
||||
actualSizeUSD: actualPositionSizeUSD,
|
||||
})
|
||||
|
||||
console.log(`💾 Trade saved with quality score: ${qualityResult.score}/100`)
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
console.log('🔄 Position sync requested...')
|
||||
|
||||
const config = getMergedConfig()
|
||||
const driftService = await getDriftService()
|
||||
const driftService = await initializeDriftService()
|
||||
const positionManager = await getInitializedPositionManager()
|
||||
const prisma = getPrismaClient()
|
||||
|
||||
|
||||
@@ -235,8 +235,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
maxAdverseExcursion: 0,
|
||||
maxFavorablePrice: entryPrice,
|
||||
maxAdversePrice: entryPrice,
|
||||
atrAtEntry: undefined,
|
||||
runnerTrailingPercent: undefined,
|
||||
priceCheckCount: 0,
|
||||
lastPrice: entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
|
||||
@@ -293,8 +293,8 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<
|
||||
// For orders that close a long, the order direction should be SHORT (sell)
|
||||
const orderDirection = options.direction === 'long' ? PositionDirection.SHORT : PositionDirection.LONG
|
||||
|
||||
// Place TP1 LIMIT reduce-only (skip if tp1Price is 0 - runner system)
|
||||
if (tp1USD > 0 && options.tp1Price > 0) {
|
||||
// Place TP1 LIMIT reduce-only
|
||||
if (tp1USD > 0) {
|
||||
const baseAmount = usdToBase(tp1USD)
|
||||
if (baseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
|
||||
const orderParams: any = {
|
||||
@@ -315,8 +315,8 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
// Place TP2 LIMIT reduce-only (skip if tp2Price is 0 - runner system)
|
||||
if (tp2USD > 0 && options.tp2Price > 0) {
|
||||
// Place TP2 LIMIT reduce-only
|
||||
if (tp2USD > 0) {
|
||||
const baseAmount = usdToBase(tp2USD)
|
||||
if (baseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
|
||||
const orderParams: any = {
|
||||
@@ -517,23 +517,19 @@ export async function closePosition(
|
||||
if (isDryRun) {
|
||||
console.log('🧪 DRY RUN MODE: Simulating close order (not executing on blockchain)')
|
||||
|
||||
// Calculate realized P&L with leverage
|
||||
// Calculate realized P&L with leverage (default 10x in dry run)
|
||||
const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1)
|
||||
const closedNotional = sizeToClose * oraclePrice
|
||||
const realizedPnL = (closedNotional * profitPercent) / 100
|
||||
const accountPnLPercent = profitPercent * 10 // display using default leverage
|
||||
|
||||
// CRITICAL FIX: closedNotional is leveraged position size, must calculate P&L on collateral
|
||||
const leverage = 10 // Default for dry run
|
||||
const collateralUsed = closedNotional / leverage
|
||||
const accountPnLPercent = profitPercent * leverage
|
||||
const realizedPnL = (collateralUsed * accountPnLPercent) / 100
|
||||
const mockTxSig = `DRY_RUN_CLOSE_${Date.now()}_${Math.random().toString(36).substring(7)}`
|
||||
|
||||
console.log(`💰 Simulated close:`)
|
||||
console.log(` Close price: $${oraclePrice.toFixed(4)}`)
|
||||
console.log(` Profit %: ${profitPercent.toFixed(3)}% → Account P&L (${leverage}x): ${accountPnLPercent.toFixed(2)}%`)
|
||||
console.log(` Profit %: ${profitPercent.toFixed(3)}% → Account P&L (10x): ${accountPnLPercent.toFixed(2)}%`)
|
||||
console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
|
||||
|
||||
const mockTxSig = `DRY_RUN_CLOSE_${Date.now()}_${Math.random().toString(36).substring(7)}`
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionSignature: mockTxSig,
|
||||
@@ -573,7 +569,7 @@ export async function closePosition(
|
||||
console.log('✅ Transaction confirmed on-chain')
|
||||
|
||||
// Calculate realized P&L with leverage
|
||||
// CRITICAL: P&L must account for leverage and be calculated on collateral, not notional
|
||||
// CRITICAL: P&L must account for leverage and be calculated on USD notional, not base asset size
|
||||
const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1)
|
||||
|
||||
// Get leverage from user account (defaults to 10x if not found)
|
||||
@@ -588,11 +584,10 @@ export async function closePosition(
|
||||
console.log('⚠️ Could not determine leverage from account, using 10x default')
|
||||
}
|
||||
|
||||
// Calculate closed notional value (USD) and convert to collateral
|
||||
// Calculate closed notional value (USD)
|
||||
const closedNotional = sizeToClose * oraclePrice
|
||||
const collateralUsed = closedNotional / leverage // CRITICAL FIX: Calculate P&L on collateral
|
||||
const accountPnLPercent = profitPercent * leverage // Account P&L includes leverage
|
||||
const realizedPnL = (collateralUsed * accountPnLPercent) / 100
|
||||
const realizedPnL = (closedNotional * profitPercent) / 100
|
||||
const accountPnLPercent = profitPercent * leverage
|
||||
|
||||
console.log(`💰 Close details:`)
|
||||
console.log(` Close price: $${oraclePrice.toFixed(4)}`)
|
||||
|
||||
@@ -35,7 +35,6 @@ export interface ActiveTrade {
|
||||
slMovedToBreakeven: boolean
|
||||
slMovedToProfit: boolean
|
||||
trailingStopActive: boolean
|
||||
runnerTrailingPercent?: number // Latest dynamic trailing percent applied
|
||||
|
||||
// P&L tracking
|
||||
realizedPnL: number
|
||||
@@ -53,7 +52,6 @@ export interface ActiveTrade {
|
||||
originalAdx?: number // ADX at initial entry (for scaling validation)
|
||||
timesScaled?: number // How many times position has been scaled
|
||||
totalScaleAdded?: number // Total USD added through scaling
|
||||
atrAtEntry?: number // ATR (absolute) when trade was opened
|
||||
|
||||
// Monitoring
|
||||
priceCheckCount: number
|
||||
@@ -119,7 +117,6 @@ export class PositionManager {
|
||||
slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false,
|
||||
slMovedToProfit: pmState?.slMovedToProfit ?? false,
|
||||
trailingStopActive: pmState?.trailingStopActive ?? false,
|
||||
runnerTrailingPercent: pmState?.runnerTrailingPercent,
|
||||
realizedPnL: pmState?.realizedPnL ?? 0,
|
||||
unrealizedPnL: pmState?.unrealizedPnL ?? 0,
|
||||
peakPnL: pmState?.peakPnL ?? 0,
|
||||
@@ -128,7 +125,6 @@ export class PositionManager {
|
||||
maxAdverseExcursion: pmState?.maxAdverseExcursion ?? 0,
|
||||
maxFavorablePrice: pmState?.maxFavorablePrice ?? dbTrade.entryPrice,
|
||||
maxAdversePrice: pmState?.maxAdversePrice ?? dbTrade.entryPrice,
|
||||
atrAtEntry: dbTrade.atrAtEntry ?? undefined,
|
||||
priceCheckCount: 0,
|
||||
lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
@@ -136,12 +132,6 @@ export class PositionManager {
|
||||
|
||||
this.activeTrades.set(activeTrade.id, activeTrade)
|
||||
console.log(`✅ Restored trade: ${activeTrade.symbol} ${activeTrade.direction} at $${activeTrade.entryPrice}`)
|
||||
|
||||
// Consistency check: if TP1 hit but SL not moved to breakeven, fix it now
|
||||
if (activeTrade.tp1Hit && !activeTrade.slMovedToBreakeven) {
|
||||
console.log(`🔧 Detected inconsistent state: TP1 hit but SL not at breakeven. Fixing now...`)
|
||||
await this.handlePostTp1Adjustments(activeTrade, 'recovery after restore')
|
||||
}
|
||||
}
|
||||
|
||||
if (this.activeTrades.size > 0) {
|
||||
@@ -213,22 +203,6 @@ export class PositionManager {
|
||||
return Array.from(this.activeTrades.values())
|
||||
}
|
||||
|
||||
async reconcileTrade(symbol: string): Promise<void> {
|
||||
const trade = Array.from(this.activeTrades.values()).find(t => t.symbol === symbol)
|
||||
if (!trade) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const driftService = getDriftService()
|
||||
const marketConfig = getMarketConfig(symbol)
|
||||
const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
|
||||
await this.checkTradeConditions(trade, oraclePrice)
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Failed to reconcile trade for ${symbol}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific trade
|
||||
*/
|
||||
@@ -342,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
|
||||
@@ -359,7 +336,12 @@ export class PositionManager {
|
||||
trade.tp1Hit = true
|
||||
trade.currentSize = positionSizeUSD
|
||||
|
||||
await this.handlePostTp1Adjustments(trade, 'on-chain TP1 detection')
|
||||
// Move SL to breakeven after TP1
|
||||
trade.stopLossPrice = trade.entryPrice
|
||||
trade.slMovedToBreakeven = true
|
||||
console.log(`🛡️ Stop loss moved to breakeven: $${trade.stopLossPrice.toFixed(4)}`)
|
||||
|
||||
await this.saveTradeState(trade)
|
||||
|
||||
} else if (trade.tp1Hit && !trade.tp2Hit && reductionPercent >= 85) {
|
||||
// TP2 fired (total should be ~95% closed, 5% runner left)
|
||||
@@ -367,22 +349,19 @@ export class PositionManager {
|
||||
trade.tp2Hit = true
|
||||
trade.currentSize = positionSizeUSD
|
||||
trade.trailingStopActive = true
|
||||
trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade)
|
||||
console.log(
|
||||
`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%`
|
||||
)
|
||||
console.log(`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with ${this.config.trailingStopPercent}% trailing stop`)
|
||||
|
||||
await this.saveTradeState(trade)
|
||||
|
||||
// CRITICAL: Don't return early! Continue monitoring the runner position
|
||||
// The trailing stop logic at line 732 needs to run
|
||||
|
||||
} else {
|
||||
// Partial fill detected but unclear which TP - just update size
|
||||
console.log(`⚠️ Unknown partial fill detected - updating tracked size to $${positionSizeUSD.toFixed(2)}`)
|
||||
trade.currentSize = positionSizeUSD
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
// Continue monitoring the remaining position
|
||||
return
|
||||
}
|
||||
|
||||
// CRITICAL: Check for entry price mismatch (NEW position opened)
|
||||
@@ -404,10 +383,10 @@ export class PositionManager {
|
||||
trade.lastPrice,
|
||||
trade.direction
|
||||
)
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
const estimatedPnL = (trade.currentSize * accountPnL) / 100
|
||||
const accountPnLPercent = profitPercent * trade.leverage
|
||||
const estimatedPnL = (trade.currentSize * profitPercent) / 100
|
||||
|
||||
console.log(`💰 Estimated P&L for lost trade: ${profitPercent.toFixed(2)}% price → ${accountPnL.toFixed(2)}% account → $${estimatedPnL.toFixed(2)} realized`)
|
||||
console.log(`💰 Estimated P&L for lost trade: ${profitPercent.toFixed(2)}% price → ${accountPnLPercent.toFixed(2)}% account → $${estimatedPnL.toFixed(2)} realized`)
|
||||
|
||||
try {
|
||||
await updateTradeExit({
|
||||
@@ -448,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
|
||||
@@ -457,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%)`)
|
||||
}
|
||||
@@ -466,41 +449,22 @@ export class PositionManager {
|
||||
// CRITICAL: Use trade state flags, not current price (on-chain orders filled in the past!)
|
||||
let exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' = 'SL'
|
||||
|
||||
// Calculate P&L first (set to 0 for phantom trades)
|
||||
let realizedPnL = 0
|
||||
let exitPrice = currentPrice
|
||||
|
||||
// Include any previously realized profit (e.g., from TP1 partial close)
|
||||
const previouslyRealized = trade.realizedPnL
|
||||
let runnerRealized = 0
|
||||
let runnerProfitPercent = 0
|
||||
if (!wasPhantom) {
|
||||
// For external closures, try to estimate a more realistic exit price
|
||||
// Manual closures may happen at significantly different prices than current market
|
||||
const unrealizedPnL = trade.unrealizedPnL || 0
|
||||
const positionSizeUSD = trade.positionSize
|
||||
|
||||
if (Math.abs(unrealizedPnL) > 1 && positionSizeUSD > 0) {
|
||||
// If we have meaningful unrealized P&L, back-calculate the likely exit price
|
||||
// This is more accurate than using volatile current market price
|
||||
const impliedProfitPercent = (unrealizedPnL / positionSizeUSD) * 100 / trade.leverage
|
||||
exitPrice = trade.direction === 'long'
|
||||
? trade.entryPrice * (1 + impliedProfitPercent / 100)
|
||||
: trade.entryPrice * (1 - impliedProfitPercent / 100)
|
||||
|
||||
console.log(`📊 Estimated exit price based on unrealized P&L:`)
|
||||
console.log(` Unrealized P&L: $${unrealizedPnL.toFixed(2)}`)
|
||||
console.log(` Market price: $${currentPrice.toFixed(6)}`)
|
||||
console.log(` Estimated exit: $${exitPrice.toFixed(6)}`)
|
||||
|
||||
realizedPnL = unrealizedPnL
|
||||
} else {
|
||||
// Fallback to current price calculation
|
||||
const profitPercent = this.calculateProfitPercent(
|
||||
trade.entryPrice,
|
||||
currentPrice,
|
||||
trade.direction
|
||||
)
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
realizedPnL = (sizeForPnL * accountPnL) / 100
|
||||
}
|
||||
runnerProfitPercent = this.calculateProfitPercent(
|
||||
trade.entryPrice,
|
||||
currentPrice,
|
||||
trade.direction
|
||||
)
|
||||
runnerRealized = (sizeForPnL * runnerProfitPercent) / 100
|
||||
}
|
||||
|
||||
const totalRealizedPnL = previouslyRealized + runnerRealized
|
||||
trade.realizedPnL = totalRealizedPnL
|
||||
console.log(` Realized P&L snapshot → Previous: $${previouslyRealized.toFixed(2)} | Runner: $${runnerRealized.toFixed(2)} (Δ${runnerProfitPercent.toFixed(2)}%) | Total: $${totalRealizedPnL.toFixed(2)}`)
|
||||
|
||||
// Determine exit reason from trade state and P&L
|
||||
if (trade.tp2Hit) {
|
||||
@@ -509,14 +473,14 @@ export class PositionManager {
|
||||
} else if (trade.tp1Hit) {
|
||||
// TP1 was hit, position should be 25% size, but now fully closed
|
||||
// This means either TP2 filled or runner got stopped out
|
||||
exitReason = realizedPnL > 0 ? 'TP2' : 'SL'
|
||||
exitReason = totalRealizedPnL > 0 ? 'TP2' : 'SL'
|
||||
} else {
|
||||
// No TPs hit yet - either SL or TP1 filled just now
|
||||
// Use P&L to determine: positive = TP, negative = SL
|
||||
if (realizedPnL > trade.positionSize * 0.005) {
|
||||
if (totalRealizedPnL > trade.positionSize * 0.005) {
|
||||
// More than 0.5% profit - must be TP1
|
||||
exitReason = 'TP1'
|
||||
} else if (realizedPnL < 0) {
|
||||
} else if (totalRealizedPnL < 0) {
|
||||
// Loss - must be SL
|
||||
exitReason = 'SL'
|
||||
}
|
||||
@@ -528,9 +492,9 @@ export class PositionManager {
|
||||
try {
|
||||
await updateTradeExit({
|
||||
positionId: trade.positionId,
|
||||
exitPrice: exitPrice, // Use estimated exit price, not current market price
|
||||
exitPrice: currentPrice,
|
||||
exitReason,
|
||||
realizedPnL,
|
||||
realizedPnL: totalRealizedPnL,
|
||||
exitOrderTx: 'ON_CHAIN_ORDER',
|
||||
holdTimeSeconds,
|
||||
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
|
||||
@@ -540,7 +504,7 @@ export class PositionManager {
|
||||
maxFavorablePrice: trade.maxFavorablePrice,
|
||||
maxAdversePrice: trade.maxAdversePrice,
|
||||
})
|
||||
console.log(`💾 External closure recorded: ${exitReason} at $${exitPrice.toFixed(6)} | P&L: $${realizedPnL.toFixed(2)}`)
|
||||
console.log(`💾 External closure recorded: ${exitReason} at $${currentPrice} | P&L: $${totalRealizedPnL.toFixed(2)}`)
|
||||
} catch (dbError) {
|
||||
console.error('❌ Failed to save external closure:', dbError)
|
||||
}
|
||||
@@ -551,31 +515,50 @@ export class PositionManager {
|
||||
}
|
||||
|
||||
// Position exists but size mismatch (partial close by TP1?)
|
||||
const onChainBaseSize = Math.abs(position.size)
|
||||
const onChainSizeUSD = onChainBaseSize * currentPrice
|
||||
const trackedSizeUSD = trade.currentSize
|
||||
|
||||
if (trackedSizeUSD > 0 && onChainSizeUSD < trackedSizeUSD * 0.95) { // 5% tolerance
|
||||
const expectedBaseSize = trackedSizeUSD / currentPrice
|
||||
console.log(`⚠️ Position size mismatch: tracking $${trackedSizeUSD.toFixed(2)} (~${expectedBaseSize.toFixed(4)} units) but on-chain shows $${onChainSizeUSD.toFixed(2)} (${onChainBaseSize.toFixed(4)} units)`)
|
||||
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 = trackedSizeUSD > 0 ? onChainSizeUSD / trackedSizeUSD : 0
|
||||
const sizeRatio = (position.size * currentPrice) / trade.currentSize
|
||||
if (sizeRatio < 0.5) {
|
||||
const tradeAgeSeconds = (Date.now() - trade.entryTime) / 1000
|
||||
const probablyPartialRunner = trade.tp1Hit || tradeAgeSeconds > 60
|
||||
|
||||
if (probablyPartialRunner) {
|
||||
console.log(`🛠️ Detected stray remainder (${(sizeRatio * 100).toFixed(1)}%) after on-chain exit - forcing market close`)
|
||||
trade.currentSize = onChainSizeUSD
|
||||
await this.saveTradeState(trade)
|
||||
await this.executeExit(trade, 100, 'manual', currentPrice)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`🚨 EXTREME SIZE MISMATCH (${(sizeRatio * 100).toFixed(1)}%) - Closing phantom trade`)
|
||||
console.log(` Expected: $${trackedSizeUSD.toFixed(2)}`)
|
||||
console.log(` Actual: $${onChainSizeUSD.toFixed(2)}`)
|
||||
console.log(` Expected: $${trade.currentSize.toFixed(2)}`)
|
||||
console.log(` Actual: $${(position.size * currentPrice).toFixed(2)}`)
|
||||
|
||||
// Close as phantom trade
|
||||
try {
|
||||
@@ -603,15 +586,10 @@ export class PositionManager {
|
||||
return
|
||||
}
|
||||
|
||||
// Update current size to match reality and run TP1 adjustments if needed
|
||||
trade.currentSize = onChainSizeUSD
|
||||
if (!trade.tp1Hit) {
|
||||
trade.tp1Hit = true
|
||||
await this.handlePostTp1Adjustments(trade, 'on-chain TP1 size sync')
|
||||
} else {
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
return
|
||||
// Update current size to match reality (convert base asset size to USD using current price)
|
||||
trade.currentSize = position.size * currentPrice
|
||||
trade.tp1Hit = true
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -636,8 +614,8 @@ export class PositionManager {
|
||||
trade.direction
|
||||
)
|
||||
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
trade.unrealizedPnL = (trade.currentSize * accountPnL) / 100
|
||||
const accountPnL = profitPercent * trade.leverage
|
||||
trade.unrealizedPnL = (trade.currentSize * profitPercent) / 100
|
||||
|
||||
// Track peak P&L (MFE - Maximum Favorable Excursion)
|
||||
if (trade.unrealizedPnL > trade.peakPnL) {
|
||||
@@ -702,7 +680,56 @@ export class PositionManager {
|
||||
// Move SL based on breakEvenTriggerPercent setting
|
||||
trade.tp1Hit = true
|
||||
trade.currentSize = trade.positionSize * ((100 - this.config.takeProfit1SizePercent) / 100)
|
||||
await this.handlePostTp1Adjustments(trade, 'software TP1 execution')
|
||||
const newStopLossPrice = this.calculatePrice(
|
||||
trade.entryPrice,
|
||||
this.config.breakEvenTriggerPercent, // Use configured breakeven level
|
||||
trade.direction
|
||||
)
|
||||
trade.stopLossPrice = newStopLossPrice
|
||||
trade.slMovedToBreakeven = true
|
||||
|
||||
console.log(`🔒 SL moved to +${this.config.breakEvenTriggerPercent}% (${this.config.takeProfit1SizePercent}% closed, ${100 - this.config.takeProfit1SizePercent}% remaining): ${newStopLossPrice.toFixed(4)}`)
|
||||
|
||||
// CRITICAL: Cancel old on-chain SL orders and place new ones at updated price
|
||||
try {
|
||||
console.log('🗑️ Cancelling old stop loss orders...')
|
||||
const { cancelAllOrders, placeExitOrders } = await import('../drift/orders')
|
||||
const cancelResult = await cancelAllOrders(trade.symbol)
|
||||
if (cancelResult.success) {
|
||||
console.log(`✅ Cancelled ${cancelResult.cancelledCount || 0} old orders`)
|
||||
|
||||
// Place new SL orders at breakeven/profit level for remaining position
|
||||
console.log(`🛡️ Placing new SL orders at $${newStopLossPrice.toFixed(4)} for remaining position...`)
|
||||
const exitOrdersResult = await placeExitOrders({
|
||||
symbol: trade.symbol,
|
||||
positionSizeUSD: trade.currentSize,
|
||||
entryPrice: trade.entryPrice,
|
||||
tp1Price: trade.tp2Price, // Only TP2 remains
|
||||
tp2Price: trade.tp2Price, // Dummy, won't be used
|
||||
stopLossPrice: newStopLossPrice,
|
||||
tp1SizePercent: 100, // Close remaining 25% at TP2
|
||||
tp2SizePercent: 0,
|
||||
direction: trade.direction,
|
||||
useDualStops: this.config.useDualStops,
|
||||
softStopPrice: trade.direction === 'long'
|
||||
? newStopLossPrice * 1.005 // 0.5% above for long
|
||||
: newStopLossPrice * 0.995, // 0.5% below for short
|
||||
hardStopPrice: newStopLossPrice,
|
||||
})
|
||||
|
||||
if (exitOrdersResult.success) {
|
||||
console.log('✅ New SL orders placed on-chain at updated price')
|
||||
} else {
|
||||
console.error('❌ Failed to place new SL orders:', exitOrdersResult.error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to update on-chain SL orders:', error)
|
||||
// Don't fail the TP1 exit if SL update fails - software monitoring will handle it
|
||||
}
|
||||
|
||||
// Save state after TP1
|
||||
await this.saveTradeState(trade)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -727,39 +754,42 @@ export class PositionManager {
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
// 5. TP2 Hit - Activate runner (no close, just start trailing)
|
||||
// 5. Take profit 2 (remaining position)
|
||||
if (trade.tp1Hit && !trade.tp2Hit && this.shouldTakeProfit2(currentPrice, trade)) {
|
||||
console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}% - Activating 25% runner!`)
|
||||
console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`)
|
||||
|
||||
// Mark TP2 as hit and activate trailing stop on full remaining 25%
|
||||
trade.tp2Hit = true
|
||||
trade.peakPrice = currentPrice
|
||||
trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade)
|
||||
// Calculate how much to close based on TP2 size percent
|
||||
const percentToClose = this.config.takeProfit2SizePercent
|
||||
|
||||
console.log(
|
||||
`🏃 Runner activated on full remaining position: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% | trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%`
|
||||
)
|
||||
await this.executeExit(trade, percentToClose, 'TP2', currentPrice)
|
||||
|
||||
// Save state after TP2 activation
|
||||
await this.saveTradeState(trade)
|
||||
// If some position remains, mark TP2 as hit and activate trailing stop
|
||||
if (percentToClose < 100) {
|
||||
trade.tp2Hit = true
|
||||
trade.currentSize = trade.currentSize * ((100 - percentToClose) / 100)
|
||||
|
||||
console.log(`🏃 Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`)
|
||||
|
||||
// Save state after TP2
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
return
|
||||
} // 6. Trailing stop for runner (after TP2 activation)
|
||||
}
|
||||
|
||||
// 6. Trailing stop for runner (after TP2)
|
||||
if (trade.tp2Hit && this.config.useTrailingStop) {
|
||||
// Check if trailing stop should be activated
|
||||
if (!trade.trailingStopActive && profitPercent >= this.config.trailingStopActivation) {
|
||||
trade.trailingStopActive = true
|
||||
trade.runnerTrailingPercent = this.getRunnerTrailingPercent(trade)
|
||||
console.log(`🎯 Trailing stop activated at +${profitPercent.toFixed(2)}%`)
|
||||
}
|
||||
|
||||
// If trailing stop is active, adjust SL dynamically
|
||||
if (trade.trailingStopActive) {
|
||||
const trailingPercent = this.getRunnerTrailingPercent(trade)
|
||||
trade.runnerTrailingPercent = trailingPercent
|
||||
const trailingStopPrice = this.calculatePrice(
|
||||
trade.peakPrice,
|
||||
-trailingPercent, // Trail below peak
|
||||
-this.config.trailingStopPercent, // Trail below peak
|
||||
trade.direction
|
||||
)
|
||||
|
||||
@@ -772,7 +802,7 @@ export class PositionManager {
|
||||
const oldSL = trade.stopLossPrice
|
||||
trade.stopLossPrice = trailingStopPrice
|
||||
|
||||
console.log(`📈 Trailing SL updated: ${oldSL.toFixed(4)} → ${trailingStopPrice.toFixed(4)} (${trailingPercent.toFixed(3)}% below peak $${trade.peakPrice.toFixed(4)})`)
|
||||
console.log(`📈 Trailing SL updated: ${oldSL.toFixed(4)} → ${trailingStopPrice.toFixed(4)} (${this.config.trailingStopPercent}% below peak $${trade.peakPrice.toFixed(4)})`)
|
||||
|
||||
// Save state after trailing SL update (every 10 updates to avoid spam)
|
||||
if (trade.priceCheckCount % 10 === 0) {
|
||||
@@ -813,37 +843,18 @@ export class PositionManager {
|
||||
return
|
||||
}
|
||||
|
||||
const closePriceForCalc = result.closePrice || currentPrice
|
||||
const closedSizeBase = result.closedSize || 0
|
||||
const closedUSD = closedSizeBase * closePriceForCalc
|
||||
const treatAsFullClose = percentToClose >= 100
|
||||
|
||||
// Calculate actual P&L based on entry vs exit price
|
||||
// CRITICAL: closedUSD is NOTIONAL value (with leverage), must calculate based on collateral
|
||||
const profitPercent = this.calculateProfitPercent(trade.entryPrice, closePriceForCalc, trade.direction)
|
||||
const collateralUSD = closedUSD / trade.leverage // Convert notional to actual collateral used
|
||||
const accountPnLPercent = profitPercent * trade.leverage // Account P&L includes leverage effect
|
||||
const actualRealizedPnL = (collateralUSD * accountPnLPercent) / 100
|
||||
|
||||
// Update trade state
|
||||
if (treatAsFullClose) {
|
||||
trade.realizedPnL += actualRealizedPnL
|
||||
trade.currentSize = 0
|
||||
trade.trailingStopActive = false
|
||||
|
||||
if (reason === 'TP2') {
|
||||
trade.tp2Hit = true
|
||||
}
|
||||
if (reason === 'TP1') {
|
||||
trade.tp1Hit = true
|
||||
}
|
||||
|
||||
if (percentToClose >= 100) {
|
||||
// Full close - remove from monitoring
|
||||
trade.realizedPnL += result.realizedPnL || 0
|
||||
|
||||
// Save to database (only for valid exit reasons)
|
||||
if (reason !== 'error') {
|
||||
try {
|
||||
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
|
||||
await updateTradeExit({
|
||||
positionId: trade.positionId,
|
||||
exitPrice: closePriceForCalc,
|
||||
exitPrice: result.closePrice || currentPrice,
|
||||
exitReason: reason as 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency',
|
||||
realizedPnL: trade.realizedPnL,
|
||||
exitOrderTx: result.transactionSignature || 'MANUAL_CLOSE',
|
||||
@@ -858,23 +869,25 @@ export class PositionManager {
|
||||
console.log('💾 Trade saved to database')
|
||||
} catch (dbError) {
|
||||
console.error('❌ Failed to save trade exit to database:', dbError)
|
||||
// Don't fail the close if database fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.removeTrade(trade.id)
|
||||
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
|
||||
} else {
|
||||
// Partial close (TP1) - calculate P&L for partial amount
|
||||
// CRITICAL: Same fix as above - closedUSD is notional, must use collateral
|
||||
const partialCollateralUSD = closedUSD / trade.leverage
|
||||
const partialAccountPnL = profitPercent * trade.leverage
|
||||
const partialRealizedPnL = (partialCollateralUSD * partialAccountPnL) / 100
|
||||
trade.realizedPnL += partialRealizedPnL
|
||||
// Partial close (TP1)
|
||||
trade.realizedPnL += result.realizedPnL || 0
|
||||
// result.closedSize is returned in base asset units (e.g., SOL), convert to USD using closePrice
|
||||
const closePriceForCalc = result.closePrice || currentPrice
|
||||
const closedSizeBase = result.closedSize || 0
|
||||
const closedUSD = closedSizeBase * closePriceForCalc
|
||||
trade.currentSize = Math.max(0, trade.currentSize - closedUSD)
|
||||
|
||||
console.log(
|
||||
`✅ Partial close executed | Realized: $${partialRealizedPnL.toFixed(2)} | Closed (base): ${closedSizeBase.toFixed(6)} | Closed (USD): $${closedUSD.toFixed(2)} | Remaining USD: $${trade.currentSize.toFixed(2)}`
|
||||
)
|
||||
console.log(`✅ Partial close executed | Realized: $${(result.realizedPnL || 0).toFixed(2)} | Closed (base): ${closedSizeBase.toFixed(6)} | Closed (USD): $${closedUSD.toFixed(2)} | Remaining USD: $${trade.currentSize.toFixed(2)}`)
|
||||
|
||||
// Persist updated trade state so analytics reflect partial profits immediately
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
// TODO: Send notification
|
||||
@@ -964,150 +977,6 @@ export class PositionManager {
|
||||
console.log('✅ All positions closed')
|
||||
}
|
||||
|
||||
refreshConfig(): void {
|
||||
this.config = getMergedConfig()
|
||||
console.log('⚙️ Position manager config refreshed from environment')
|
||||
}
|
||||
|
||||
private getRunnerTrailingPercent(trade: ActiveTrade): number {
|
||||
const fallbackPercent = this.config.trailingStopPercent
|
||||
const atrValue = trade.atrAtEntry ?? 0
|
||||
const entryPrice = trade.entryPrice
|
||||
|
||||
if (atrValue <= 0 || entryPrice <= 0 || !Number.isFinite(entryPrice)) {
|
||||
return fallbackPercent
|
||||
}
|
||||
|
||||
const atrPercentOfPrice = (atrValue / entryPrice) * 100
|
||||
if (!Number.isFinite(atrPercentOfPrice) || atrPercentOfPrice <= 0) {
|
||||
return fallbackPercent
|
||||
}
|
||||
|
||||
const rawPercent = atrPercentOfPrice * this.config.trailingStopAtrMultiplier
|
||||
const boundedPercent = Math.min(
|
||||
this.config.trailingStopMaxPercent,
|
||||
Math.max(this.config.trailingStopMinPercent, rawPercent)
|
||||
)
|
||||
|
||||
return boundedPercent > 0 ? boundedPercent : fallbackPercent
|
||||
}
|
||||
|
||||
private async handlePostTp1Adjustments(trade: ActiveTrade, context: string): Promise<void> {
|
||||
if (trade.currentSize <= 0) {
|
||||
console.log(`⚠️ Skipping TP1 adjustments for ${trade.symbol} (${context}) because current size is $${trade.currentSize.toFixed(2)}`)
|
||||
await this.saveTradeState(trade)
|
||||
return
|
||||
}
|
||||
|
||||
const newStopLossPrice = this.calculatePrice(
|
||||
trade.entryPrice,
|
||||
this.config.breakEvenTriggerPercent,
|
||||
trade.direction
|
||||
)
|
||||
|
||||
trade.stopLossPrice = newStopLossPrice
|
||||
trade.slMovedToBreakeven = true
|
||||
|
||||
console.log(`🔒 (${context}) SL moved to +${this.config.breakEvenTriggerPercent}% (${this.config.takeProfit1SizePercent}% closed, ${100 - this.config.takeProfit1SizePercent}% remaining): ${newStopLossPrice.toFixed(4)}`)
|
||||
|
||||
// CRITICAL FIX: For runner system (tp2SizePercent=0), don't place any TP orders
|
||||
// The remaining 25% should only have stop loss and be managed by software trailing stop
|
||||
const shouldPlaceTpOrders = this.config.takeProfit2SizePercent > 0
|
||||
|
||||
if (shouldPlaceTpOrders) {
|
||||
// Traditional system: place TP2 order for remaining position
|
||||
await this.refreshExitOrders(trade, {
|
||||
stopLossPrice: newStopLossPrice,
|
||||
tp1Price: trade.tp2Price,
|
||||
tp1SizePercent: 100,
|
||||
tp2Price: trade.tp2Price,
|
||||
tp2SizePercent: 0,
|
||||
context,
|
||||
})
|
||||
} else {
|
||||
// Runner system: Only place stop loss, no TP orders
|
||||
// The 25% runner will be managed by software trailing stop
|
||||
console.log(`🏃 Runner system active - placing ONLY stop loss at breakeven, no TP orders`)
|
||||
await this.refreshExitOrders(trade, {
|
||||
stopLossPrice: newStopLossPrice,
|
||||
tp1Price: 0, // No TP1 order
|
||||
tp1SizePercent: 0,
|
||||
tp2Price: 0, // No TP2 order
|
||||
tp2SizePercent: 0,
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
await this.saveTradeState(trade)
|
||||
}
|
||||
|
||||
private async refreshExitOrders(
|
||||
trade: ActiveTrade,
|
||||
options: {
|
||||
stopLossPrice: number
|
||||
tp1Price: number
|
||||
tp1SizePercent: number
|
||||
tp2Price?: number
|
||||
tp2SizePercent?: number
|
||||
context: string
|
||||
}
|
||||
): Promise<void> {
|
||||
if (trade.currentSize <= 0) {
|
||||
console.log(`⚠️ Skipping exit order refresh for ${trade.symbol} (${options.context}) because tracked size is zero`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🗑️ (${options.context}) Cancelling existing exit orders before refresh...`)
|
||||
const { cancelAllOrders, placeExitOrders } = await import('../drift/orders')
|
||||
const cancelResult = await cancelAllOrders(trade.symbol)
|
||||
if (cancelResult.success) {
|
||||
console.log(`✅ (${options.context}) Cancelled ${cancelResult.cancelledCount || 0} old orders`)
|
||||
} else {
|
||||
console.warn(`⚠️ (${options.context}) Failed to cancel old orders: ${cancelResult.error}`)
|
||||
}
|
||||
|
||||
const tp2Price = options.tp2Price ?? options.tp1Price
|
||||
const tp2SizePercent = options.tp2SizePercent ?? 0
|
||||
|
||||
const refreshParams: any = {
|
||||
symbol: trade.symbol,
|
||||
positionSizeUSD: trade.currentSize,
|
||||
entryPrice: trade.entryPrice,
|
||||
tp1Price: options.tp1Price,
|
||||
tp2Price,
|
||||
stopLossPrice: options.stopLossPrice,
|
||||
tp1SizePercent: options.tp1SizePercent,
|
||||
tp2SizePercent,
|
||||
direction: trade.direction,
|
||||
useDualStops: this.config.useDualStops,
|
||||
}
|
||||
|
||||
if (this.config.useDualStops) {
|
||||
const softStopBuffer = this.config.softStopBuffer ?? 0.4
|
||||
const softStopPrice = trade.direction === 'long'
|
||||
? options.stopLossPrice * (1 + softStopBuffer / 100)
|
||||
: options.stopLossPrice * (1 - softStopBuffer / 100)
|
||||
|
||||
refreshParams.softStopPrice = softStopPrice
|
||||
refreshParams.softStopBuffer = softStopBuffer
|
||||
refreshParams.hardStopPrice = options.stopLossPrice
|
||||
}
|
||||
|
||||
console.log(`🛡️ (${options.context}) Placing refreshed exit orders: size=$${trade.currentSize.toFixed(2)} SL=${options.stopLossPrice.toFixed(4)} TP=${options.tp1Price.toFixed(4)}`)
|
||||
const exitOrdersResult = await placeExitOrders(refreshParams)
|
||||
|
||||
if (exitOrdersResult.success) {
|
||||
console.log(`✅ (${options.context}) Exit orders refreshed on-chain`)
|
||||
} else {
|
||||
console.error(`❌ (${options.context}) Failed to place refreshed exit orders: ${exitOrdersResult.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ (${options.context}) Error refreshing exit orders:`, error)
|
||||
// Monitoring loop will still enforce SL logic even if on-chain refresh fails
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save trade state to database (for persistence across restarts)
|
||||
*/
|
||||
@@ -1131,6 +1000,14 @@ export class PositionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload configuration from merged sources (used after settings updates)
|
||||
*/
|
||||
refreshConfig(partial?: Partial<TradingConfig>): void {
|
||||
this.config = getMergedConfig(partial)
|
||||
console.log('🔄 Position Manager config refreshed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monitoring status
|
||||
*/
|
||||
|
||||
@@ -140,16 +140,8 @@ export function scoreSignalQuality(params: {
|
||||
}
|
||||
|
||||
// Price position check (avoid chasing vs breakout detection)
|
||||
// CRITICAL: Low price position (< 40%) + weak trend (ADX < 25) = range-bound chop
|
||||
if (params.pricePosition > 0) {
|
||||
const isWeakTrend = params.adx > 0 && params.adx < 25
|
||||
const isLowInRange = params.pricePosition < 40
|
||||
|
||||
// ANTI-CHOP: Heavily penalize range-bound entries
|
||||
if (isLowInRange && isWeakTrend) {
|
||||
score -= 25
|
||||
reasons.push(`⚠️ RANGE-BOUND CHOP: Low position (${params.pricePosition.toFixed(0)}%) + weak trend (ADX ${params.adx.toFixed(1)}) = high whipsaw risk`)
|
||||
} else if (params.direction === 'long' && params.pricePosition > 95) {
|
||||
if (params.direction === 'long' && params.pricePosition > 95) {
|
||||
// High volume breakout at range top can be good
|
||||
if (params.volumeRatio > 1.4) {
|
||||
score += 5
|
||||
@@ -173,15 +165,13 @@ export function scoreSignalQuality(params: {
|
||||
}
|
||||
}
|
||||
|
||||
// Volume breakout bonus - ONLY if trend is strong enough (not choppy)
|
||||
// Removed old logic that conflicted with anti-chop filter
|
||||
// Old bonus was rewarding high volume even during choppy markets
|
||||
if (params.volumeRatio > 1.8 && params.atr < 0.6 && params.adx > 18) {
|
||||
// Volume breakout bonus (high volume can override other weaknesses)
|
||||
if (params.volumeRatio > 1.8 && params.atr < 0.6) {
|
||||
score += 10
|
||||
reasons.push(`Volume breakout compensates for low ATR (ADX ${params.adx.toFixed(1)} confirms trend)`)
|
||||
reasons.push(`Volume breakout compensates for low ATR`)
|
||||
}
|
||||
|
||||
const minScore = params.minScore || 65
|
||||
const minScore = params.minScore || 60
|
||||
const passed = score >= minScore
|
||||
|
||||
return {
|
||||
|
||||
@@ -367,7 +367,7 @@
|
||||
},
|
||||
{
|
||||
"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 formats: \"ETH buy 15\", \"SOL buy .P 5\", etc.\n// The .P is optional TradingView ticker suffix that should be ignored\nconst timeframeMatch = body.match(/\\b(buy|sell)\\s+(?:\\.P\\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};"
|
||||
"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};"
|
||||
},
|
||||
"id": "81f28bc7-c96a-4021-acac-242e993d9d98",
|
||||
"name": "Parse Signal Enhanced",
|
||||
|
||||
Reference in New Issue
Block a user