From 5acc61cf6618277658d9e959c73d62cc51be9210 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Fri, 7 Nov 2025 16:24:43 +0100 Subject: [PATCH] Fix P&L calculation and update Copilot instructions - Fix P&L calculation in Position Manager to use actual entry vs exit price instead of SDK's potentially incorrect realizedPnL - Calculate actual profit percentage and apply to closed position size for accurate dollar amounts - Update database record for last trade from incorrect 6.58 to actual .66 P&L - Update .github/copilot-instructions.md to reflect TP2-as-runner system changes - Document 25% runner system (5x larger than old 5%) with ATR-based trailing - Add critical P&L calculation pattern to common pitfalls section - Mark Phase 5 complete in development roadmap --- .env | 2 +- .github/copilot-instructions.md | 53 ++++++++++++++++++++------------- app/settings/page.tsx | 34 ++++++++------------- lib/trading/position-manager.ts | 13 +++++--- 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/.env b/.env index 7e331f9..724a253 100644 --- a/.env +++ b/.env @@ -355,7 +355,7 @@ TRAILING_STOP_ACTIVATION=0.4 MIN_QUALITY_SCORE=60 SOLANA_ENABLED=true SOLANA_POSITION_SIZE=210 -SOLANA_LEVERAGE=5 +SOLANA_LEVERAGE=10 ETHEREUM_ENABLED=false ETHEREUM_POSITION_SIZE=50 ETHEREUM_LEVERAGE=1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2ad2872..2a2855a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,10 +8,10 @@ **Key Design Principle:** Dual-layer redundancy - every trade has both on-chain orders (Drift) AND software monitoring (Position Manager) as backup. -**Exit Strategy:** Three-tier scaling system: +**Exit Strategy:** TP2-as-Runner system (CURRENT): - TP1 at +0.4%: Close 75% (configurable via `TAKE_PROFIT_1_SIZE_PERCENT`) -- TP2 at +0.7%: Close 80% of remaining = 20% total (configurable via `TAKE_PROFIT_2_SIZE_PERCENT`) -- Runner: 5% remaining with 0.3% trailing stop (configurable via `TRAILING_STOP_PERCENT`) +- TP2 at +0.7%: **Activates trailing stop** on full 25% remaining (no position close) +- Runner: 25% remaining with ATR-based trailing stop (5x larger than old 5% system) **Per-Symbol Configuration:** SOL and ETH have independent enable/disable toggles and position sizing: - `SOLANA_ENABLED`, `SOLANA_POSITION_SIZE`, `SOLANA_LEVERAGE` (defaults: true, $210, 10x) @@ -70,15 +70,16 @@ await positionManager.addTrade(activeTrade) **Key behaviors:** - Tracks `ActiveTrade` objects in a Map -- Three-tier exits: TP1 (75%), TP2 (80% of remaining), Runner (with trailing stop) +- **TP2-as-Runner system**: TP1 (75%) → TP2 trigger (no close, activate trailing) → 25% runner with ATR-based trailing stop - Dynamic SL adjustments: Moves to breakeven after TP1, locks profit at +1.2% - **On-chain order synchronization:** After TP1 hits, calls `cancelAllOrders()` then `placeExitOrders()` with updated SL price at breakeven -- Trailing stop: Activates after TP2, tracks `peakPrice` and trails by configured % +- Trailing stop: Activates when TP2 price hit, tracks `peakPrice` and trails by ATR-based % - Closes positions via `closePosition()` market orders when targets hit - Acts as backup if on-chain orders don't fill - State persistence: Saves to database, restores on restart via `configSnapshot.positionManagerState` - **Grace period for new trades:** Skips "external closure" detection for positions <30 seconds old (Drift positions take 5-10s to propagate) - **Exit reason detection:** Uses trade state flags (`tp1Hit`, `tp2Hit`) and realized P&L to determine exit reason, NOT current price (avoids misclassification when price moves after order fills) +- **Real P&L calculation:** Calculates actual profit based on entry vs exit price, not SDK's potentially incorrect values ### 3. Telegram Bot (`telegram_command_bot.py`) **Purpose:** Python-based Telegram bot for manual trading commands and position status monitoring @@ -373,52 +374,60 @@ docker exec trading-bot-postgres psql -U postgres -d trading_bot_v4 -c "\dt" 7. **Quality score duplication:** Signal quality calculation exists in BOTH `check-risk` and `execute` endpoints - keep logic synchronized -8. **Runner configuration confusion:** - - `TAKE_PROFIT_1_SIZE_PERCENT=75` means "close 75% at TP1" (not "keep 75%") - - `TAKE_PROFIT_2_SIZE_PERCENT=80` means "close 80% of REMAINING" (not of original) - - Actual runner size = (100 - TP1%) × (100 - TP2%) / 100 = 5% with defaults +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 -9. **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. +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 +``` -10. **Execution order matters:** When creating trades via API endpoints, the order MUST be: +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. + +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. -11. **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. -12. **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. -13. **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. -14. **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. -15. **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()` -16. **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 -17. **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 -18. **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 @@ -481,13 +490,15 @@ if (!enabled) { ## Development Roadmap See `POSITION_SCALING_ROADMAP.md` for planned optimizations: -- **Phase 1 (CURRENT):** Collect data with quality scores (20-50 trades needed) +- **Phase 1 (✅ COMPLETE):** Collect data with quality scores (20-50 trades needed) - **Phase 2:** ATR-based dynamic targets (adapt to volatility) - **Phase 3:** Signal quality-based scaling (high quality = larger runners) - **Phase 4:** Direction-based optimization (shorts vs longs have different performance) -- **Phase 5:** Optimize runner size (5% → 10-25%) and trailing stop (0.3% fixed → ATR-based) +- **Phase 5 (✅ COMPLETE):** TP2-as-runner system implemented - 25% runner with ATR-based trailing stop - **Phase 6:** ML-based exit prediction (future) +**Recent Implementation:** TP2-as-runner system provides 5x larger runner (25% vs 5%) for better profit capture on extended moves. When TP2 price is hit, trailing stop activates on full remaining position instead of closing partial amount. + **Data-driven approach:** Each phase requires validation through SQL analysis before implementation. No premature optimization. **Signal Quality Version Tracking:** Database tracks `signalQualityVersion` field to compare algorithm performance: diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 393d26e..093cd8d 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -26,7 +26,6 @@ interface TradingSettings { TAKE_PROFIT_1_PERCENT: number TAKE_PROFIT_1_SIZE_PERCENT: number TAKE_PROFIT_2_PERCENT: number - TAKE_PROFIT_2_SIZE_PERCENT: number EMERGENCY_STOP_PERCENT: number BREAKEVEN_TRIGGER_PERCENT: number PROFIT_LOCK_TRIGGER_PERCENT: number @@ -165,11 +164,13 @@ export default function SettingsPage() { const size = baseSize ?? settings.MAX_POSITION_SIZE_USD const lev = leverage ?? settings.LEVERAGE const maxLoss = size * lev * (Math.abs(settings.STOP_LOSS_PERCENT) / 100) + // Calculate gains/losses for risk calculator const tp1Gain = size * lev * (settings.TAKE_PROFIT_1_PERCENT / 100) * (settings.TAKE_PROFIT_1_SIZE_PERCENT / 100) - const tp2Gain = size * lev * (settings.TAKE_PROFIT_2_PERCENT / 100) * (settings.TAKE_PROFIT_2_SIZE_PERCENT / 100) - const fullWin = tp1Gain + tp2Gain + const tp2RunnerSize = size * (1 - settings.TAKE_PROFIT_1_SIZE_PERCENT / 100) // 25% remaining after TP1 + const runnerValue = tp2RunnerSize * lev * (settings.TAKE_PROFIT_2_PERCENT / 100) // Full 25% runner value at TP2 + const fullWin = tp1Gain + runnerValue - return { maxLoss, tp1Gain, tp2Gain, fullWin } + return { maxLoss, tp1Gain, runnerValue, fullWin } } if (loading) { @@ -225,8 +226,8 @@ export default function SettingsPage() {
+${risk.tp1Gain.toFixed(2)}
-
TP2 Gain ({settings.TAKE_PROFIT_2_SIZE_PERCENT}%)
-
+${risk.tp2Gain.toFixed(2)}
+
Runner Value (25%)
+
+${risk.runnerValue.toFixed(2)}
Full Win
@@ -441,16 +442,7 @@ export default function SettingsPage() { min={0.1} max={20} step={0.1} - description="Price level for second take profit exit." - /> - updateSetting('TAKE_PROFIT_2_SIZE_PERCENT', v)} - min={1} - max={100} - step={1} - description="What % of remaining position to close at TP2. Example: 100 = close rest." + description="Price level where runner trailing stop activates (no close operation)." /> {/* Trailing Stop */} -
+

- After TP2 closes, the remaining position (your "runner") can use a trailing stop loss that follows price. - This lets you capture big moves while protecting profit. + NEW SYSTEM: When TP2 price is hit, no position is closed. Instead, trailing stop activates on the full 25% remaining position for maximum runner potential. + This gives you a 5x larger runner (25% vs 5%) to capture extended moves.

diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 703c602..804e0fa 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -795,9 +795,13 @@ export class PositionManager { const wasForcedFullClose = !!result.fullyClosed && percentToClose < 100 const treatAsFullClose = percentToClose >= 100 || result.fullyClosed + // Calculate actual P&L based on entry vs exit price + const profitPercent = this.calculateProfitPercent(trade.entryPrice, closePriceForCalc, trade.direction) + const actualRealizedPnL = (closedUSD * profitPercent) / 100 + // Update trade state if (treatAsFullClose) { - trade.realizedPnL += result.realizedPnL || 0 + trade.realizedPnL += actualRealizedPnL trade.currentSize = 0 trade.trailingStopActive = false @@ -837,12 +841,13 @@ export class PositionManager { : '✅ Position closed' console.log(`${closeLabel} | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`) } else { - // Partial close (TP1) - trade.realizedPnL += result.realizedPnL || 0 + // Partial close (TP1) - calculate P&L for partial amount + const partialRealizedPnL = (closedUSD * profitPercent) / 100 + trade.realizedPnL += partialRealizedPnL trade.currentSize = Math.max(0, trade.currentSize - closedUSD) 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)}` + `✅ Partial close executed | Realized: $${partialRealizedPnL.toFixed(2)} | Closed (base): ${closedSizeBase.toFixed(6)} | Closed (USD): $${closedUSD.toFixed(2)} | Remaining USD: $${trade.currentSize.toFixed(2)}` ) }