Commit Graph

61 Commits

Author SHA1 Message Date
mindesbunister
6156c0f958 critical: Fix P&L compounding bug in external closure detection
- CRITICAL BUG: trade.realizedPnL was being mutated during each external closure detection
- This caused exponential compounding: $6 → $12 → $24 → $48 → $96
- Each time monitoring loop detected closure, it added previouslyRealized + runnerRealized
- But previouslyRealized was the ALREADY ACCUMULATED value from previous iteration
- Result: P&L compounded 15-20x on actual value

ROOT CAUSE (line 797):
  const totalRealizedPnL = previouslyRealized + runnerRealized
  trade.realizedPnL = totalRealizedPnL  // ← BUG: Mutates in-memory trade object

Next detection cycle:
  const previouslyRealized = trade.realizedPnL  // ← Gets ACCUMULATED value
  const totalRealizedPnL = previouslyRealized + runnerRealized  // ← Adds AGAIN

FIX:
- Don't mutate trade.realizedPnL during external closure detection
- Calculate totalRealizedPnL locally, use for database update only
- trade.realizedPnL stays immutable after initial DB save
- Log message clarified: 'P&L calculation' not 'P&L snapshot'

IMPACT:
- Every external closure (TP/SL on-chain orders) affected
- With rate limiting, closure detected 15-20 times before removal
- Real example: $6 actual profit showed as $92.46 in database
- This is WORSE than duplicate notification bug - corrupts financial data

FILES CHANGED:
- lib/trading/position-manager.ts: Removed trade.realizedPnL mutation (line 799)
- Database manually corrected: $92.46 → $6.00 for affected trade

RELATED BUGS:
- Common Pitfall #48: closingInProgress flag prevents some duplicates
- But doesn't help if monitoring loop runs DURING external closure detection
- Need both fixes: closingInProgress + no mutation of trade.realizedPnL
2025-11-17 15:28:08 +01:00
mindesbunister
3aeb00f998 critical: Fix P&L calculation and TP1 false detection bugs
- Add originalPositionSize tracking to prevent stale size usage
- Add price validation to TP1 detection (prevents manual closes misidentified as TP1)
- Fix external closure P&L to use originalPositionSize not currentSize
- Add handleManualClosure method for proper exit reason detection
- Add isPriceAtTarget helper for TP/SL price validation (0.2% tolerance)
- Update all ActiveTrade creation points (execute, test, sync-positions, test-db)

Bug fixes:
- Manual close at 42.34 was detected as TP1 (target 40.71) - FIXED
- P&L showed -$1.71 instead of actual -$2.92 - FIXED
- Exit reason showed SL instead of manual - FIXED

Root cause: Position Manager detected size reduction without validating
price was actually at TP1 level. Used stale currentSize for P&L calculation.

Files modified:
- lib/trading/position-manager.ts (core fixes)
- app/api/trading/execute/route.ts
- app/api/trading/test/route.ts
- app/api/trading/sync-positions/route.ts
- app/api/trading/test-db/route.ts
2025-11-17 15:10:15 +01:00
mindesbunister
018f973609 critical: Fix P&L compounding during close verification (20x inflation bug)
Problem:
- Close transaction confirmed but Drift state takes 5-10s to propagate
- Position Manager returned needsVerification=true to keep monitoring
- BUT: Monitoring loop detected position as 'externally closed' EVERY 2 seconds
- Each detection called handleExternalClosure() and added P&L to database
- Result: .66 actual profit → 73.36 in database (20x compounding)
- Logs showed: $112.96 → $117.62 → $122.28 → ... → $173.36 (14+ updates)

Root Cause:
- Common Pitfall #47 fix introduced needsVerification flag to wait for propagation
- But NO flag to prevent external closure detection during wait period
- Monitoring loop thought position was 'closed externally' on every cycle
- Rate limiting (429 errors) made it worse by extending wait time

Fix (closingInProgress flag):
1. Added closingInProgress boolean to ActiveTrade interface
2. Set flag=true when needsVerification returned (close confirmed, waiting)
3. Skip external closure detection entirely while flag=true
4. Timeout after 60s if stuck (abnormal case - allows cleanup)

Impact:
- Every close with verification delay (most closes) had 10-20x P&L inflation
- This is variant of Common Pitfall #27 but during verification, not external closure
- Rate limited closes were hit hardest (longer wait = more compounding cycles)

Files:
- lib/trading/position-manager.ts: Added closingInProgress flag + skip logic

Incident: Nov 16, 11:50 CET - SHORT 41.64→40.08 showed 73.36 vs .66 real
Documented: Common Pitfall #48
2025-11-16 15:07:27 +01:00
mindesbunister
c607a66239 critical: Fix position close verification to prevent ghost positions
Problem:
- Close transaction confirmed on-chain BUT Drift state takes 5-10s to propagate
- Position Manager immediately checked position after close → still showed open
- Continued monitoring with stale state → eventually ghost detected
- Database marked 'SL closed' but position actually stayed open for 6+ hours
- Position was UNPROTECTED during this time (no monitoring, no TP/SL backup)

Root Cause:
- Transaction confirmation ≠ Drift internal state updated
- SDK needs time to propagate on-chain changes to internal cache
- Position Manager assumed immediate state consistency

Fix (2-layer verification):
1. closePosition(): After 100% close confirmation, wait 5s then verify
   - Query Drift to confirm position actually gone
   - If still exists: Return needsVerification=true flag
   - Log CRITICAL error with transaction signature

2. Position Manager: Handle needsVerification flag
   - DON'T mark position closed in database
   - DON'T remove from monitoring
   - Keep monitoring until ghost detection sees it's actually closed
   - Prevents premature cleanup with wrong exit data

Impact:
- Prevents 6-hour unmonitored position exposure
- Ensures database exit data matches actual Drift closure
- Ghost detection becomes safety net, not primary close mechanism
- User positions always protected until VERIFIED closed

Files:
- lib/drift/orders.ts: Added 5s wait + position verification after close
- lib/trading/position-manager.ts: Check needsVerification flag before cleanup

Incident: Nov 16, 02:51 - Close confirmed but position stayed open until 08:51
2025-11-16 10:00:10 +01:00
mindesbunister
673a49302a critical: Fix breakeven SL using wrong entry price after TP1
- CRITICAL BUG: Drift SDK's position.entryPrice RECALCULATES after partial closes
- After TP1, Drift returns COST BASIS of remaining position, NOT original entry
- Example: SHORT @ 38.52 → TP1 @ 70% → Drift shows entry 40.01 (runner's basis)
- Result: Breakeven SL set .50 ABOVE actual entry = guaranteed loss if triggered

Fix:
- Always use database trade.entryPrice for breakeven calculations
- Drift's position.entryPrice = current state (runner cost basis)
- Database entryPrice = original entry (authoritative for breakeven)
- Added logging to show both values for verification

Impact:
- Every TP1 → breakeven transition was using WRONG price
- Locking in losses instead of true breakeven protection
- Financial loss bug affecting every trade with TP1

Files:
- lib/trading/position-manager.ts: Line 513 - use trade.entryPrice not position.entryPrice
- .github/copilot-instructions.md: Added Common Pitfall #43, deprecated old #44

Incident: Nov 16, 02:47 CET - SHORT entry 38.52, breakeven SL set at 40.01
Position closed by ghost detection before SL could trigger (lucky)
2025-11-16 03:00:22 +01:00
mindesbunister
528a0f4f43 fix: Use Drift's actual entry price for breakeven SL
Problem: After TP1, SL moved to 'breakeven' but used database entry price,
which can differ from Drift's actual fill price by /bin/bash.10-0.15.

Example:
- DB stored: $139.18291 entry
- Drift actual: $139.07 entry
- SL set to: $139.18 (DB value)

Solution: Query position.entryPrice from Drift SDK when setting breakeven SL.
Drift SDK calculates entry from on-chain data (quoteAssetAmount / baseAssetAmount)
which is more accurate than database stored value.

Code change (lib/trading/position-manager.ts line ~511):
- Before: trade.stopLossPrice = trade.entryPrice
- After: trade.stopLossPrice = position.entryPrice || trade.entryPrice

Impact: TRUE breakeven protection - no slippage losses from price discrepancies.

Related: Common Pitfall #33 (orphaned position restoration with wrong entry price)
2025-11-16 01:44:57 +01:00
mindesbunister
b1ca454a6f feat: Add Telegram notifications for position closures
Implemented direct Telegram notifications when Position Manager closes positions:
- New helper: lib/notifications/telegram.ts with sendPositionClosedNotification()
- Integrated into Position Manager's executeExit() for all closure types
- Also sends notifications for ghost position cleanups

Notification includes:
- Symbol, direction, entry/exit prices
- P&L amount and percentage
- Position size and hold time
- Exit reason (TP1, TP2, SL, manual, ghost cleanup, etc.)
- MAE/MFE stats (max gain/drawdown during trade)

User request: Receive P&L notifications on position closures via Telegram bot
Previously: Only opening notifications via n8n workflow
Now: All closures (TP/SL/manual/ghost) send notifications directly
2025-11-16 00:51:56 +01:00
mindesbunister
9db5f8566d refactor: Remove time-based ghost detection, rely purely on Drift API
User feedback: Time-based cleanup (6 hours) too aggressive for legitimate long-running positions.
Drift API is the authoritative source of truth.

Changes:
- Removed cleanupStalePositions() method entirely
- Removed age-based Layer 1 from validatePositions()
- Updated Layer 2: Now verifies with Drift API before removing position
- All ghost detection now uses Drift blockchain as source of truth

Ghost detection methods:
- Layer 2: Queries Drift after 20 failed close attempts
- Layer 3: Queries Drift every 40 seconds during monitoring
- Periodic validation: Queries Drift every 5 minutes

Result: No premature closures, more reliable ghost detection.
2025-11-16 00:22:19 +01:00
mindesbunister
4779a9f732 fix: 3-layer ghost position prevention system (CRITICAL autonomous reliability fix)
PROBLEM: Ghost positions caused death spirals
- Position Manager tracked 2 positions that were actually closed
- Caused massive rate limit storms (100+ RPC calls)
- Telegram /status showed wrong data
- Periodic validation SKIPPED during rate limiting (fatal flaw)
- Created death spiral: ghosts → rate limits → validation skipped → more rate limits

USER REQUIREMENT: "bot has to work all the time especially when i am not on my laptop"
- System MUST be fully autonomous
- Must self-heal from ghost accumulation
- Cannot rely on manual container restarts

SOLUTION: 3-layer protection system (Nov 15, 2025)

**LAYER 1: Database-based age check**
- Runs every 5 minutes during validation
- Removes positions >6 hours old (likely ghosts)
- Doesn't require RPC calls - ALWAYS works even during rate limiting
- Prevents long-term ghost accumulation

**LAYER 2: Death spiral detector**
- Monitors close attempt failures during rate limiting
- After 20+ failed close attempts (40+ seconds), forces removal
- Breaks rate limit death spirals immediately
- Prevents infinite retry loops

**LAYER 3: Monitoring loop integration**
- Every 20 price checks (~40 seconds), verifies position exists on Drift
- Catches ghosts quickly during normal monitoring
- No 5-minute wait - immediate detection
- Silently skips check during RPC errors (no log spam)

**Key fixes:**
- validatePositions(): Now runs database cleanup FIRST before Drift checks
- Changed 'skipping validation' to 'using database-only validation'
- Added cleanupStalePositions() function (>6h age threshold)
- Added death spiral detection in executeExit() rate limit handler
- Added ghost check in checkTradeConditions() every 20 price updates
- All layers work together - if one fails, others protect

**Impact:**
- System now self-healing - no manual intervention needed
- Ghost positions cleaned within 40-360 seconds (depending on layer)
- Works even during severe rate limiting (Layer 1 always runs)
- Telegram /status always shows correct data
- User can be away from laptop - bot handles itself

**Testing:**
- Container restart cleared ghosts (as expected - DB shows all closed)
- New fixes will prevent future accumulation autonomously

Files changed:
- lib/trading/position-manager.ts (3 layers added)
2025-11-15 23:51:19 +01:00
mindesbunister
59bc267206 fix: Add runner stop loss protection (CRITICAL)
- CRITICAL BUG: Position Manager only checked SL before TP1
- After TP1 hit, runner had NO stop loss protection
- Added separate SL check for runner (after TP1, before TP2)
- Runner now protected by profit-lock SL on Position Manager

Bug discovered: Runner position with no on-chain orders (below min size)
AND no software protection (SL check skipped after TP1).

Impact: 2.79 runner exposed to unlimited loss for 10+ minutes.
Fix: Added line 881-886 runner SL check in monitoring loop.
2025-11-15 22:10:41 +01:00
mindesbunister
5b2ec408a8 fix: Update on-chain SL to breakeven after TP1 hit (CRITICAL)
CRITICAL BUG: After TP1 filled, Position Manager updated internal
stopLossPrice but NEVER updated the actual on-chain orders on Drift.
Runner had NO real stop loss protection at breakeven.

Fix:
- After TP1 detection, call cancelAllOrders() to remove old orders
- Then call placeExitOrders() with updated SL at breakeven
- Place TP2 as new TP1 for runner (activates trailing at that level)
- Logs: 'Cancelling old exit orders', 'Placing new exit orders'

Impact: Runner now properly protected at breakeven on-chain, not just
in Position Manager tracking.

Found: User screenshot showed SL still at original levels (46.57)
after TP1 hit, when it should have been at entry (42.89).
2025-11-15 19:37:05 +01:00
mindesbunister
d236e08cc0 feat: Add periodic Drift position validation to prevent ghost positions
- Added 5-minute validation interval to Position Manager
- Validates tracked positions against actual Drift state
- Auto-cleanup ghost positions (DB shows open but Drift shows closed)
- Prevents rate limit storms from accumulated ghost positions
- Logs detailed ghost detection: DB state vs Drift state
- Self-healing system requires no manual intervention

Implementation:
- scheduleValidation(): Sets 5-minute timer after monitoring starts
- validatePositions(): Queries each tracked position on Drift
- handleExternalClosure(): Reusable method for ghost cleanup
- Clears interval when monitoring stops

Benefits:
- Prevents ghost position accumulation
- Eliminates need for manual container restarts
- Minimal RPC overhead (1 check per 5 min per position)
- Addresses root cause (state management) not symptom (rate limits)

Fixes:
- Ghost positions from failed DB updates during external closures
- Container restart state sync issues
- Rate limit exhaustion from managing non-existent positions
2025-11-15 19:20:51 +01:00
mindesbunister
ec5483041a fix(CRITICAL): Add missing stop loss check for runner between TP1 and TP2
CRITICAL BUG: Runner had NO stop loss protection between TP1 and TP2!

Impact: Runner position completely unprotected for entire TP1→TP2 window
Risk: Unlimited loss exposure on 25-30% remaining position

Example: SHORT at $141.31, TP1 closed 70% at $140.94, runner has SL at $140.89
- Price rises to $141.98 (way above SL) → NO STOP LOSS CHECK → Losses accumulate
- Should have closed at $140.89 with 0.3% profit locked

Fix: Added explicit stop loss check for runner state (TP1 hit but TP2 not hit)
Log: "🔴 RUNNER STOP LOSS" to distinguish from pre-TP1 stops

Files: lib/trading/position-manager.ts
2025-11-15 11:28:54 +01:00
mindesbunister
324e5ba002 refactor: Rename breakEvenTriggerPercent to profitLockAfterTP1Percent for clarity
- Renamed config variable to accurately reflect behavior (locks profit, not breakeven)
- Updated log messages to say 'lock +X% profit' instead of misleading 'breakeven'
- Maintains backwards compatibility (accepts old BREAKEVEN_TRIGGER_PERCENT env var)
- Updated .env with new variable name and explanatory comment

Why: Config was named 'breakeven' but actually locks profit at entry ± X%
For SHORT at $141.51 with 0.3% lock: SL moves to $141.08 (not breakeven $141.51)
This protects remaining runner position after TP1 by allowing small profit giveback

Files changed:
- config/trading.ts: Interface + default + env parsing
- lib/trading/position-manager.ts: Usage + log message
- .env: Variable rename with migration comment
2025-11-15 11:06:44 +01:00
mindesbunister
7ff78ee0bd feat: Hybrid RPC fallback system (Alchemy → Helius)
- Automatic fallback after 2 consecutive rate limits
- Primary: Alchemy (300M CU/month, stable for normal ops)
- Fallback: Helius (10 req/sec, backup for startup bursts)
- Reduced startup validation: 6h window, 5 trades (was 24h, 20 trades)
- Multi-position safety check (prevents order cancellation conflicts)
- Rate limit-aware retry logic with exponential backoff

Implementation:
- lib/drift/client.ts: Added fallbackConnection, switchToFallbackRpc()
- .env: SOLANA_FALLBACK_RPC_URL configuration
- lib/startup/init-position-manager.ts: Reduced validation scope
- lib/trading/position-manager.ts: Multi-position order protection

Tested: System switched to fallback on startup, Position Manager active
Result: 1 active trade being monitored after automatic RPC switch
2025-11-14 15:28:07 +01:00
mindesbunister
27eb5d4fe8 fix: Critical rate limit handling + startup position restoration
**Problem 1: Rate Limit Cascade**
- Position Manager tried to close repeatedly, overwhelming Helius RPC (10 req/s limit)
- Base retry delay was too aggressive (2s → 4s → 8s)
- No graceful handling when 429 errors occur

**Problem 2: Orphaned Positions After Restart**
- Container restarts lost Position Manager state
- Positions marked 'closed' in DB but still open on Drift (failed close transactions)
- No cross-validation between database and actual Drift positions

**Solutions Implemented:**

1. **Increased retry delays (orders.ts)**:
   - Base delay: 2s → 5s (progression now 5s → 10s → 20s)
   - Reduces RPC pressure during rate limit situations
   - Gives Helius time to recover between retries
   - Documented Helius limits: 100 req/s burst, 10 req/s sustained (free tier)

2. **Startup position validation (init-position-manager.ts)**:
   - Cross-checks last 24h of 'closed' trades against actual Drift positions
   - If DB says closed but Drift shows open → reopens in DB to restore tracking
   - Prevents unmonitored positions from existing after container restarts
   - Logs detailed mismatch info for debugging

3. **Rate limit-aware exit handling (position-manager.ts)**:
   - Detects 429 errors during position close
   - Keeps trade in monitoring instead of removing it
   - Natural retry on next price update (vs aggressive 2s loop)
   - Prevents marking position as closed when transaction actually failed

**Impact:**
- Eliminates orphaned positions after restarts
- Reduces RPC pressure by 2.5x (5s vs 2s base delay)
- Graceful degradation under rate limits
- Position Manager continues monitoring even during temporary RPC issues

**Testing needed:**
- Monitor next container restart to verify position restoration works
- Check rate limit analytics after next close attempt
- Verify no more phantom 'closed' positions when Drift shows open
2025-11-14 09:50:13 +01:00
mindesbunister
31bc08bed4 fix: TP1/TP2 race condition causing multiple simultaneous closures
CRITICAL BUG FIX:
- Position Manager monitoring loop (every 2s) could trigger TP1/TP2 multiple times
- tp1Hit flag was set AFTER async executeExit() completed
- Multiple concurrent executeExit() calls happened before flag was set
- Result: Position closed 6 times (70% close × 6 = entire position + failed attempts)

ROOT CAUSE:
- Race window: ~0.5-1s between check and flag set
- Multiple monitoring loops entered if statement simultaneously

FIX APPLIED:
- Set tp1Hit = true IMMEDIATELY before calling executeExit()
- Same fix for tp2Hit flag
- Prevents concurrent execution by setting flag synchronously

EVIDENCE:
- Test trade at 04:47:09: TP1 triggered 6 times
- First close: Remaining $13.52 (correct 30%)
- Closes 2-6: Remaining $0.00 (closed entire position)
- Position Manager continued tracking $13.02 runner that didn't exist

IMPACT:
- User had unprotected $42.73 position (Position Manager tracking phantom)
- No TP/SL monitoring, no trailing stop
- Had to manually close position

Files changed:
- lib/trading/position-manager.ts: Move tp1Hit/tp2Hit flag setting before async calls
- Prevents race condition on all future trades

Testing required: Execute test trade and verify TP1 triggers only once.
2025-11-14 06:26:59 +01:00
mindesbunister
bd9633fbc2 CRITICAL FIX: Prevent unprotected positions via database-first pattern
Root Cause:
- Execute endpoint saved to database AFTER adding to Position Manager
- Database save failures were silently caught and ignored
- API returned success even when DB save failed
- Container restarts lost in-memory Position Manager state
- Result: Unprotected positions with no TP/SL monitoring

Fixes Applied:

1. Database-First Pattern (app/api/trading/execute/route.ts):
   - MOVED createTrade() BEFORE positionManager.addTrade()
   - If database save fails, return HTTP 500 with critical error
   - Error message: 'CLOSE POSITION MANUALLY IMMEDIATELY'
   - Position Manager only tracks database-persisted trades
   - Ensures container restarts can restore all positions

2. Transaction Timeout (lib/drift/orders.ts):
   - Added 30s timeout to confirmTransaction() in closePosition()
   - Prevents API from hanging during network congestion
   - Uses Promise.race() pattern for timeout enforcement

3. Telegram Error Messages (telegram_command_bot.py):
   - Parse JSON for ALL responses (not just 200 OK)
   - Extract detailed error messages from 'message' field
   - Shows critical warnings to user immediately
   - Fail-open: proceeds if analytics check fails

4. Position Manager (lib/trading/position-manager.ts):
   - Move lastPrice update to TOP of monitoring loop
   - Ensures /status endpoint always shows current price

Verification:
- Test trade cmhxj8qxl0000od076m21l58z executed successfully
- Database save completed BEFORE Position Manager tracking
- SL triggered correctly at -$4.21 after 15 minutes
- All protection systems working as expected

Impact:
- Eliminates risk of unprotected positions
- Provides immediate critical warnings if DB fails
- Enables safe container restarts with full position recovery
- Verified with live test trade on production

See: CRITICAL_INCIDENT_UNPROTECTED_POSITION.md for full incident report
2025-11-13 15:56:28 +01:00
mindesbunister
a21ae6d622 Add v7-momentum indicator (experimental, disabled)
- Created momentum scalper indicator for catching rapid price acceleration
- ROC-based detection: 2.0% threshold over 5 bars
- Volume confirmation: 2.0x spike (checks last 3 bars)
- ADX filter: Requires 12+ minimum directional movement
- Anti-chop filter: Blocks signals in dead markets
- Debug table: Real-time metric display for troubleshooting

Status: Functional but signal quality inferior to v6 HalfTrend
Decision: Shelved for now, continue with proven v6 strategy
File: docs/guides/MOMENTUM_INDICATOR_V1.pine (239 lines)

Lessons learned:
- Momentum indicators inherently noisy (40-50% WR expected)
- Signals either too early (false breakouts) or too late (miss move)
- Volume spike timing issue: Often lags price move by 1-2 bars
- Better to optimize proven strategy than add complexity

Related: Position Manager duplicate update bug fixed (awaiting verification)
2025-11-12 19:55:19 +01:00
mindesbunister
bba58da8fa CRITICAL FIX: position.size is tokens not USD
Fixed Position Manager incorrectly treating position.size as USD when
Drift SDK actually returns base asset tokens (SOL, ETH, BTC).

Impact:
- FALSE TP1 detections (12.28 SOL misinterpreted as 2.28 USD)
- Stop loss moved to breakeven prematurely
- Runner system activated incorrectly
- Positions stuck in wrong state

Changes:
- Line 322: Convert position.size to USD: position.size * currentPrice
- Line 519: Calculate positionSizeUSD before comparison
- Line 558: Use positionSizeUSD directly (already in USD)
- Line 591: Save positionSizeUSD (no price multiplication needed)

Before: Compared 12.28 tokens < 1950 USD = 99.4% reduction = FALSE TP1

This was causing current trade to think TP1 hit when position is still 100% open.
2025-11-12 11:30:47 +01:00
mindesbunister
03e91fc18d feat: ATR-based trailing stop + rate limit monitoring
MAJOR FIXES:
- ATR-based trailing stop for runners (was fixed 0.3%, now adapts to volatility)
- Fixes runners with +7-9% MFE exiting for losses
- Typical improvement: 2.24x more room (0.3% → 0.67% at 0.45% ATR)
- Enhanced rate limit logging with database tracking
- New /api/analytics/rate-limits endpoint for monitoring

DETAILS:
- Position Manager: Calculate trailing as (atrAtEntry / price × 100) × multiplier
- Config: TRAILING_STOP_ATR_MULTIPLIER=1.5, MIN=0.25%, MAX=0.9%
- Settings UI: Added ATR multiplier controls
- Rate limits: Log hits/recoveries/exhaustions to SystemEvent table
- Documentation: ATR_TRAILING_STOP_FIX.md + RATE_LIMIT_MONITORING.md

IMPACT:
- Runners can now capture big moves (like morning's $172→$162 SOL drop)
- Rate limit visibility prevents silent failures
- Data-driven optimization for RPC endpoint health
2025-11-11 14:51:41 +01:00
mindesbunister
c3a053df63 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.
2025-11-10 19:46:03 +01:00
mindesbunister
988fdb9ea4 Fix runner system + strengthen anti-chop filter
Three critical bugs fixed:
1. P&L calculation (65x inflation) - now uses collateralUSD not notional
2. handlePostTp1Adjustments() - checks tp2SizePercent===0 for runner mode
3. JavaScript || operator bug - changed to ?? for proper 0 handling

Signal quality improvements:
- Added anti-chop filter: price position <40% + ADX <25 = -25 points
- Prevents range-bound flip-flops (caught all 3 today)
- Backtest: 43.8% → 55.6% win rate, +86% profit per trade

Changes:
- lib/trading/signal-quality.ts: RANGE-BOUND CHOP penalty
- lib/drift/orders.ts: Fixed P&L calculation + transaction confirmation
- lib/trading/position-manager.ts: Runner system logic
- app/api/trading/execute/route.ts: || to ?? for tp2SizePercent
- app/api/trading/test/route.ts: || to ?? for tp1/tp2SizePercent
- prisma/schema.prisma: Added collateralUSD field
- scripts/fix_pnl_calculations.sql: Historical P&L correction
2025-11-10 15:36:51 +01:00
mindesbunister
6f0a1bb49b feat: Implement percentage-based position sizing
- Add usePercentageSize flag to SymbolSettings and TradingConfig
- Add calculateActualPositionSize() and getActualPositionSizeForSymbol() helpers
- Update execute and test endpoints to calculate position size from free collateral
- Add SOLANA_USE_PERCENTAGE_SIZE, ETHEREUM_USE_PERCENTAGE_SIZE, USE_PERCENTAGE_SIZE env vars
- Configure SOL to use 100% of portfolio (auto-adjusts to available balance)
- Fix TypeScript errors: replace fillNotionalUSD with actualSizeUSD
- Remove signalQualityVersion and fullyClosed references (not in interfaces)
- Add comprehensive documentation in PERCENTAGE_SIZING_FEATURE.md

Benefits:
- Prevents insufficient collateral errors by using available balance
- Auto-scales positions as account grows/shrinks
- Maintains risk proportional to capital
- Flexible per-symbol configuration (SOL percentage, ETH fixed)
2025-11-10 13:35:10 +01:00
mindesbunister
22195ed34c Fix P&L calculation and signal flip detection
- Fix external closure P&L using tp1Hit flag instead of currentSize
- Add direction change detection to prevent false TP1 on signal flips
- Signal flips now recorded with accurate P&L as 'manual' exits
- Add retry logic with exponential backoff for Solana RPC rate limits
- Create /api/trading/cancel-orders endpoint for manual cleanup
- Improves data integrity for win/loss statistics
2025-11-09 17:59:50 +01:00
mindesbunister
9b767342dc feat: Implement re-entry analytics system with fresh TradingView data
- Add market data cache service (5min expiry) for storing TradingView metrics
- Create /api/trading/market-data webhook endpoint for continuous data updates
- Add /api/analytics/reentry-check endpoint for validating manual trades
- Update execute endpoint to auto-cache metrics from incoming signals
- Enhance Telegram bot with pre-execution analytics validation
- Support --force flag to override analytics blocks
- Use fresh ADX/ATR/RSI data when available, fallback to historical
- Apply performance modifiers: -20 for losing streaks, +10 for winning
- Minimum re-entry score 55 (vs 60 for new signals)
- Fail-open design: proceeds if analytics unavailable
- Show data freshness and source in Telegram responses
- Add comprehensive setup guide in docs/guides/REENTRY_ANALYTICS_QUICKSTART.md

Phase 1 implementation for smart manual trade validation.
2025-11-07 20:40:07 +01:00
mindesbunister
6d5991172a feat: Implement ATR-based dynamic TP2 system and fix P&L calculation
- Add ATR-based dynamic TP2 scaling from 0.7% to 3.0% based on volatility
- New config options: useAtrBasedTargets, atrMultiplierForTp2, minTp2Percent, maxTp2Percent
- Enhanced settings UI with ATR controls and updated risk calculator
- Fix external closure P&L calculation using unrealized P&L instead of volatile current price
- Update execute and test endpoints to use calculateDynamicTp2() function
- Maintain 25% runner system for capturing extended moves (4-5% targets)
- Add environment variables for ATR-based configuration
- Better P&L accuracy for manual position closures
2025-11-07 17:01:22 +01:00
mindesbunister
5acc61cf66 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
2025-11-07 16:24:43 +01:00
mindesbunister
0c644ccabe Make TP2 the runner - no more partial closes
CHANGE: TP2 now activates trailing stop on full 25% remaining instead
of closing 80% and leaving 5% runner.

Benefits:
- 5x larger runner (25% vs 5%) = 25 vs 05 on 100 position
- Eliminates Drift minimum size issues completely
- Simplifies logic - no more canUseRunner() viability checks
- Better R:R on extended moves

New flow:
- TP1 (+0.4%): Close 75%, keep 25%
- TP2 (+0.7%): Skip close, activate trailing stop on full 25%
- Runner: 25% with ATR-based trailing (0.25-0.9%)

Config change: takeProfit2SizePercent: 80 → 0
Position Manager: Remove canUseRunner logic, activate trailing at TP2 hit
2025-11-07 15:29:50 +01:00
mindesbunister
36ba3809a1 Fix runner system by checking minimum position size viability
PROBLEM: Runner never activated because Drift force-closes positions below
minimum size. TP2 would close 80% leaving 5% runner (~$105), but Drift
automatically closed the entire position.

SOLUTION:
1. Created runner-calculator.ts with canUseRunner() to check if remaining
   size would be above Drift minimums BEFORE executing TP2 close
2. If runner not viable: Skip TP2 close entirely, activate trailing stop
   on full 25% remaining (from TP1)
3. If runner viable: Execute TP2 as normal, activate trailing on 5%

Benefits:
- Runner system will now actually work for viable position sizes
- Positions that are too small won't try to force-close below minimums
- Better logs showing why runner did/didn't activate
- Trailing stop works on larger % if runner not viable (better R:R)

Example: $2100 position → $525 after TP1 → $105 runner = VIABLE
         $4 ETH position → $1 after TP1 → $0.20 runner = NOT VIABLE

Runner will trail with ATR-based dynamic % (0.25-0.9%) below peak price.
2025-11-07 15:10:01 +01:00
mindesbunister
6c7eaf5f04 Add TP1/SL consistency check on trade restore 2025-11-06 12:18:31 +01:00
mindesbunister
7c888282ec Adjust TP detection logic for partial fills 2025-11-05 23:49:41 +01:00
mindesbunister
5241920d44 Prevent repeated TP2 cleanup loops 2025-11-05 16:14:17 +01:00
mindesbunister
a100945864 Enhance trailing stop with ATR-based sizing 2025-11-05 15:28:12 +01:00
mindesbunister
149294084e fix: auto-clean leftovers after stop hits 2025-11-05 11:42:22 +01:00
mindesbunister
b58e08778e fix: correct MFE/MAE tracking after partial exits 2025-11-05 10:29:32 +01:00
mindesbunister
18e3e73e83 feat: refresh exit orders after TP1 and add dry-run harness 2025-11-05 10:00:39 +01:00
mindesbunister
cbb6592153 fix: correct PnL math and add health probe 2025-11-05 07:58:27 +01:00
mindesbunister
1426a9ec2f CRITICAL FIX: P&L calculation using wrong position size for phantom trades
- Position Manager was calculating P&L using tracked size instead of actual on-chain size
- Example: Tracked 100, actual 0.04 SOL () = -99.63% false loss instead of -0.32%
- Fixed external closure detection to use position.size * currentPrice as lastKnownSize
- Manually corrected phantom trade P&L from -092.25 to /bin/bash
- Total P&L corrected: -013.92 → +8.33 (accurate)
- Prevents all future phantom/mismatch trades from wildly incorrect P&L

Modified:
- lib/trading/position-manager.ts lines 421-445 (external closure P&L calculation)
2025-11-03 16:57:53 +01:00
mindesbunister
8a8d4a348c feat: Add position scaling for strong confirmation signals
**Feature: Position Scaling**
Allows adding to existing profitable positions when high-quality signals confirm trend strength.

**Configuration (config/trading.ts):**
- enablePositionScaling: false (disabled by default - enable after testing)
- minScaleQualityScore: 75 (higher bar than initial 60)
- minProfitForScale: 0.4% (must be at/past TP1)
- maxScaleMultiplier: 2.0 (max 200% of original size)
- scaleSizePercent: 50% (add 50% of original position)
- minAdxIncrease: 5 (ADX must strengthen)
- maxPricePositionForScale: 70% (don't chase resistance)

**Validation Logic (check-risk endpoint):**
Same-direction signal triggers scaling check if enabled:
1. Quality score ≥75 (stronger than initial entry)
2. Position profitable ≥0.4% (at/past TP1)
3. ADX increased ≥5 points (trend strengthening)
4. Price position <70% (not near resistance)
5. Total size <2x original (risk management)
6. Returns 'allowed: true, reason: Position scaling' if all pass

**Execution (execute endpoint):**
- Opens additional position at scale size (50% of original)
- Updates ActiveTrade: timesScaled, totalScaleAdded, currentSize
- Tracks originalAdx from first entry for comparison
- Returns 'action: scaled' with scale details

**ActiveTrade Interface:**
Added fields:
- originalAdx?: number (for scaling validation)
- timesScaled?: number (track scaling count)
- totalScaleAdded?: number (total USD added)

**Example Scenario:**
1. LONG SOL at $176 (quality: 45, ADX: 13.4) - weak but entered
2. Price hits $176.70 (+0.4%) - at TP1
3. New LONG signal (quality: 78, ADX: 19) - strong confirmation
4. Scaling validation:  Quality 78  Profit +0.4%  ADX +5.6  Price 68%
5. Adds 50% more position at $176.70
6. Total position: 150% of original size

**Conservative Design:**
- Disabled by default (requires manual enabling)
- Only scales INTO profitable positions (never averaging down)
- Requires significant quality improvement (75 vs 60)
- Requires trend confirmation (ADX increase)
- Hard cap at 2x original size
- Won't chase near resistance levels

**Next Steps:**
1. Enable in settings: ENABLE_POSITION_SCALING=true
2. Test with small positions first
3. Monitor data: do scaled positions outperform?
4. Adjust thresholds based on results

**Safety:**
- All existing duplicate prevention logic intact
- Flip logic unchanged (still requires quality check)
- Position Manager tracks scaling state
- Can be toggled on/off without code changes
2025-11-03 15:35:33 +01:00
mindesbunister
6b1d32a72d fix: Add phantom trade detection and prevention safeguards
**Root Causes:**
1. Auto-flip logic could create phantom trades if close failed
2. Position size mismatches (0.01 SOL vs 11.92 SOL expected) not caught
3. Multiple trades for same symbol+direction in database

**Preventive Measures:**

1. **Startup Validation (lib/startup/init-position-manager.ts)**
   - Validates all open trades against Drift positions on startup
   - Auto-closes phantom trades with <50% expected size
   - Logs size mismatches for manual review
   - Prevents Position Manager from tracking ghost positions

2. **Duplicate Position Prevention (app/api/trading/execute/route.ts)**
   - Blocks opening same-direction position on same symbol
   - Returns 400 error if duplicate detected
   - Only allows auto-flip (opposite direction close + open)

3. **Runtime Phantom Detection (lib/trading/position-manager.ts)**
   - Checks position size every 2s monitoring cycle
   - Auto-closes if size ratio <50% (extreme mismatch)
   - Logs as 'manual' exit with AUTO_CLEANUP tx
   - Removes from monitoring immediately

4. **Quality Score Fix (app/api/trading/check-risk/route.ts)**
   - Hardcoded minScore=60 (removed non-existent config reference)

**Prevention Summary:**
-  Startup validation catches historical phantoms
-  Duplicate check prevents new phantoms
-  Runtime detection catches size mismatches <30s after they occur
-  All three layers work together for defense-in-depth

Issue: User had LONG (phantom) + SHORT (undersized 0.01 SOL vs 11.92 expected)
Fix: Both detected and closed, bot now clean with 0 active trades
2025-11-03 13:53:12 +01:00
mindesbunister
881a99242d feat: Add per-symbol trading controls for SOL and ETH
- Add SymbolSettings interface with enabled/positionSize/leverage fields
- Implement per-symbol ENV variables (SOLANA_*, ETHEREUM_*)
- Add SOL and ETH sections to settings UI with enable/disable toggles
- Add symbol-specific test buttons (SOL LONG/SHORT, ETH LONG/SHORT)
- Update execute and test endpoints to check symbol enabled status
- Add real-time risk/reward calculator per symbol
- Rename 'Position Sizing' to 'Global Fallback' for clarity
- Fix position manager P&L calculation for externally closed positions
- Fix zero P&L bug affecting 12 historical trades
- Add SQL scripts for recalculating historical P&L data
- Move archive TypeScript files to .archive to fix build

Defaults:
- SOL: 10 base × 10x leverage = 100 notional (profit trading)
- ETH:  base × 1x leverage =  notional (data collection)
- Global: 10 × 10x for BTC and other symbols

Configuration priority: Per-symbol ENV > Market config > Global ENV > Defaults
2025-11-03 10:28:48 +01:00
mindesbunister
0ea8773bdc fix: detect exit reason using trade state flags instead of current price
CRITICAL BUG: Position Manager was using current price to determine exit reason,
but on-chain orders filled at a DIFFERENT price in the past!

Example: LONG entry $184.55, TP1 filled at $184.66, but when Position Manager
checked later (price dropped), it saw currentPrice < TP1 and defaulted to 'SL'

Result: Profitable trades incorrectly labeled as SL exits in database

Fix:
- Use trade.tp1Hit and trade.tp2Hit flags to determine exit reason
- If no TP flags set, use realized P&L to distinguish:
  - Profit >0.5% = TP1 filled
  - Negative P&L = SL filled
- Remove duplicate P&L calculation

This ensures exit reasons match actual on-chain order fills
2025-11-03 00:02:19 +01:00
mindesbunister
ee7558b47c fix: remove duplicate line from MAE/MFE implementation 2025-11-02 23:01:34 +01:00
mindesbunister
12d874ff93 feat: implement MAE/MFE tracking for trade optimization
Added Maximum Favorable/Adverse Excursion tracking:
- Track maxFavorableExcursion: best profit % reached during trade
- Track maxAdverseExcursion: worst loss % reached during trade
- Track maxFavorablePrice and maxAdversePrice
- Update every price check (2s interval)
- Save to database on trade exit for optimization analysis

Benefits:
- Identify if TP levels are too conservative (MFE consistently higher)
- Determine if SL is too tight (MAE < SL but trade recovers)
- Optimize runner size based on how often MFE >> TP2
- Data-driven exit strategy tuning after collecting 10-20 trades

Display in monitoring logs: Shows MFE/MAE % every 20 seconds
2025-11-02 23:00:21 +01:00
mindesbunister
7c18e81164 fix: update on-chain SL orders after TP1 hits
CRITICAL: After TP1 closes 75%, the on-chain stop loss orders were NOT being updated
- Position Manager was tracking new SL price internally but not updating Drift orders
- Old SL orders (e.g., $181.69) remained active even after TP1 at $185.28
- This prevented the 'move SL to breakeven after TP1' logic from working

Fix:
- After TP1 hits, cancel ALL old orders on-chain
- Place new SL orders at updated price (breakeven + configured %)
- Place remaining TP2 order for the 25% runner position
- Maintains dual-stop system if enabled

Result: SL will now actually move up on Drift UI after TP1 fires
2025-11-02 22:41:06 +01:00
mindesbunister
bcd1cd0c76 fix(position-manager): correctly handle partial close size conversions to USD
- Convert closePosition.closedSize (base asset) to USD when updating trade.currentSize
- Fix conversion when position.size detected from Drift: set currentSize = position.size * currentPrice
- Prevent trade.currentSize from being reduced to tiny values due to unit mismatch
2025-11-02 20:34:59 +01:00
mindesbunister
466c0c8001 fix: runner tracking bug - detect TP fills by size reduction
- Position Manager now detects TP1/TP2 fills by monitoring position size reductions instead of entry price mismatches
- When position size reduces by ~75%, marks TP1 as filled and updates currentSize
- When position size reduces by ~95%, marks TP2 as filled and activates trailing stop for 5% runner
- Entry price mismatch check now skipped after TP fills (Drift shows weighted average entry price after partial closes)
- Fixes bug where runners were incorrectly closed after TP1/TP2 fired on-chain
- Adds grace period for new trades (<30s) to avoid false positives during blockchain propagation delays
- This unblocks Phase 1 data collection for signal quality optimization (need 10+ trades with MAE/MFE data)
2025-11-01 20:06:14 +01:00
mindesbunister
056440bf8f feat: add quality score display and timezone fixes
- Add qualityScore to ExecuteTradeResponse interface and response object
- Update analytics page to always show Signal Quality card (N/A if unavailable)
- Fix n8n workflow to pass context metrics and qualityScore to execute endpoint
- Fix timezone in Telegram notifications (Europe/Berlin)
- Fix symbol normalization in /api/trading/close endpoint
- Update Drift ETH-PERP minimum order size (0.002 ETH not 0.01)
- Add transaction confirmation to closePosition() to prevent phantom closes
- Add 30-second grace period for new trades in Position Manager
- Fix execution order: database save before Position Manager.addTrade()
- Update copilot instructions with transaction confirmation pattern
2025-11-01 17:00:37 +01:00
mindesbunister
8a17c2cf90 Fix Position Manager bug: prevent cancelling orders when tracking old trades
Bug: Position Manager was comparing ANY position on the symbol to the trade being
tracked, without verifying entry price match. When a new position opened, it would
think the old tracked trade 'closed externally' and cancel ALL orders - including
the new position's exit orders.

Fix: Added entry price verification (0.5% tolerance). If position entry price doesn't
match the tracked trade, mark the old trade as 'lost tracking' and remove from
monitoring WITHOUT cancelling orders (they belong to the new position).

This prevents the catastrophic scenario where exit orders are repeatedly cancelled,
leaving positions unprotected.
2025-10-31 09:34:48 +01:00