feat: phantom trade auto-closure system
- Auto-close phantom positions immediately via market order - Return HTTP 200 (not 500) to allow n8n workflow continuation - Save phantom trades to database with full P&L tracking - Exit reason: 'manual' category for phantom auto-closes - Protects user during unavailable hours (sleeping, no phone) - Add Docker build best practices to instructions (background + tail) - Document phantom system as Critical Component #1 - Add Common Pitfall #30: Phantom notification workflow Why auto-close: - User can't always respond to phantom alerts - Unmonitored position = unlimited risk exposure - Better to exit with small loss/gain than leave exposed - Re-entry possible if setup actually good Files changed: - app/api/trading/execute/route.ts: Auto-close logic - .github/copilot-instructions.md: Documentation + build pattern
This commit is contained in:
4
.env
4
.env
@@ -369,8 +369,8 @@ TRAILING_STOP_PERCENT=0.3
|
|||||||
TRAILING_STOP_ACTIVATION=0.4
|
TRAILING_STOP_ACTIVATION=0.4
|
||||||
MIN_QUALITY_SCORE=60
|
MIN_QUALITY_SCORE=60
|
||||||
SOLANA_ENABLED=true
|
SOLANA_ENABLED=true
|
||||||
SOLANA_POSITION_SIZE=100
|
SOLANA_POSITION_SIZE=50
|
||||||
SOLANA_LEVERAGE=15
|
SOLANA_LEVERAGE=1
|
||||||
SOLANA_USE_PERCENTAGE_SIZE=true
|
SOLANA_USE_PERCENTAGE_SIZE=true
|
||||||
ETHEREUM_ENABLED=false
|
ETHEREUM_ENABLED=false
|
||||||
ETHEREUM_POSITION_SIZE=50
|
ETHEREUM_POSITION_SIZE=50
|
||||||
|
|||||||
155
.github/copilot-instructions.md
vendored
155
.github/copilot-instructions.md
vendored
@@ -66,6 +66,8 @@
|
|||||||
|
|
||||||
## VERIFICATION MANDATE: Financial Code Requires Proof
|
## VERIFICATION MANDATE: Financial Code Requires Proof
|
||||||
|
|
||||||
|
**CRITICAL: THIS IS A REAL MONEY TRADING SYSTEM - NOT A TOY PROJECT**
|
||||||
|
|
||||||
**Core Principle:** In trading systems, "working" means "verified with real data", NOT "code looks correct".
|
**Core Principle:** In trading systems, "working" means "verified with real data", NOT "code looks correct".
|
||||||
|
|
||||||
**NEVER declare something working without:**
|
**NEVER declare something working without:**
|
||||||
@@ -73,6 +75,14 @@
|
|||||||
2. Verifying database state matches expectations
|
2. Verifying database state matches expectations
|
||||||
3. Comparing calculated values to source data
|
3. Comparing calculated values to source data
|
||||||
4. Testing with real trades when applicable
|
4. Testing with real trades when applicable
|
||||||
|
5. **CONFIRMING CODE IS DEPLOYED** - Check container start time vs commit time
|
||||||
|
|
||||||
|
**CODE COMMITTED ≠ CODE DEPLOYED**
|
||||||
|
- Git commit at 15:56 means NOTHING if container started at 15:06
|
||||||
|
- ALWAYS verify: `docker logs trading-bot-v4 | grep "Server starting" | head -1`
|
||||||
|
- Compare container start time to commit timestamp
|
||||||
|
- If container older than commit: **CODE NOT DEPLOYED, FIX NOT ACTIVE**
|
||||||
|
- Never say "fixed" or "protected" until deployment verified
|
||||||
|
|
||||||
### Critical Path Verification Requirements
|
### Critical Path Verification Requirements
|
||||||
|
|
||||||
@@ -206,6 +216,12 @@ Then observe logs on actual trade:
|
|||||||
|
|
||||||
### Deployment Checklist
|
### Deployment Checklist
|
||||||
|
|
||||||
|
**MANDATORY PRE-DEPLOYMENT VERIFICATION:**
|
||||||
|
- [ ] Check container start time: `docker logs trading-bot-v4 | grep "Server starting" | head -1`
|
||||||
|
- [ ] Compare to commit timestamp: Container MUST be newer than code changes
|
||||||
|
- [ ] If container older: **STOP - Code not deployed, fix not active**
|
||||||
|
- [ ] Never declare "fixed" or "working" until container restarted with new code
|
||||||
|
|
||||||
Before marking feature complete:
|
Before marking feature complete:
|
||||||
- [ ] Code review completed
|
- [ ] Code review completed
|
||||||
- [ ] Unit tests pass (if applicable)
|
- [ ] Unit tests pass (if applicable)
|
||||||
@@ -213,6 +229,7 @@ Before marking feature complete:
|
|||||||
- [ ] Logs show expected behavior
|
- [ ] Logs show expected behavior
|
||||||
- [ ] Database state verified with SQL
|
- [ ] Database state verified with SQL
|
||||||
- [ ] Edge cases tested
|
- [ ] Edge cases tested
|
||||||
|
- [ ] **Container restarted and verified running new code**
|
||||||
- [ ] Documentation updated (including Common Pitfalls if applicable)
|
- [ ] Documentation updated (including Common Pitfalls if applicable)
|
||||||
- [ ] User notified of what to verify during first real trade
|
- [ ] User notified of what to verify during first real trade
|
||||||
|
|
||||||
@@ -224,17 +241,113 @@ Before marking feature complete:
|
|||||||
- Test trade behaved differently than expected
|
- Test trade behaved differently than expected
|
||||||
- You're unsure about unit conversions or SDK behavior
|
- You're unsure about unit conversions or SDK behavior
|
||||||
- Change affects money (position sizing, P&L, exits)
|
- Change affects money (position sizing, P&L, exits)
|
||||||
|
- **Container hasn't been restarted since code commit**
|
||||||
|
|
||||||
**Instead say:**
|
**Instead say:**
|
||||||
- "Code is updated. Need to verify with test trade - watch for [specific log message]"
|
- "Code is updated. Need to verify with test trade - watch for [specific log message]"
|
||||||
- "Fixed, but requires verification: check database shows [expected value]"
|
- "Fixed, but requires verification: check database shows [expected value]"
|
||||||
- "Deployed. First real trade should show [behavior]. If not, there's still a bug."
|
- "Deployed. First real trade should show [behavior]. If not, there's still a bug."
|
||||||
|
- **"Code committed but NOT deployed - container running old version, fix not active yet"**
|
||||||
|
|
||||||
|
### Docker Build Best Practices
|
||||||
|
|
||||||
|
**CRITICAL: Prevent build interruptions with background execution + live monitoring**
|
||||||
|
|
||||||
|
Docker builds take 40-70 seconds and are easily interrupted by terminal issues. Use this pattern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start build in background with live log tail
|
||||||
|
cd /home/icke/traderv4 && docker compose build trading-bot > /tmp/docker-build-live.log 2>&1 & BUILD_PID=$!; echo "Build started, PID: $BUILD_PID"; tail -f /tmp/docker-build-live.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this works:**
|
||||||
|
- Build runs in background (`&`) - immune to terminal disconnects/Ctrl+C
|
||||||
|
- Output redirected to log file - can review later if needed
|
||||||
|
- `tail -f` shows real-time progress - see compilation, linting, errors
|
||||||
|
- Can Ctrl+C the `tail -f` without killing build - build continues
|
||||||
|
- Verification after: `tail -50 /tmp/docker-build-live.log` to check success
|
||||||
|
|
||||||
|
**Success indicators:**
|
||||||
|
- `✓ Compiled successfully in 27s`
|
||||||
|
- `✓ Generating static pages (30/30)`
|
||||||
|
- `#22 naming to docker.io/library/traderv4-trading-bot done`
|
||||||
|
- `DONE X.Xs` on final step
|
||||||
|
|
||||||
|
**Failure indicators:**
|
||||||
|
- `Failed to compile.`
|
||||||
|
- `Type error:`
|
||||||
|
- `ERROR: process "/bin/sh -c npm run build" did not complete successfully: exit code: 1`
|
||||||
|
|
||||||
|
**After successful build:**
|
||||||
|
```bash
|
||||||
|
# Deploy new container
|
||||||
|
docker compose up -d --force-recreate trading-bot
|
||||||
|
|
||||||
|
# Verify it started
|
||||||
|
docker logs --tail=30 trading-bot-v4
|
||||||
|
|
||||||
|
# Confirm deployed version
|
||||||
|
docker logs trading-bot-v4 | grep "Server starting" | head -1
|
||||||
|
```
|
||||||
|
|
||||||
|
**DO NOT use:** `docker compose build trading-bot` in foreground - one network hiccup kills 60s of work
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Critical Components
|
## Critical Components
|
||||||
|
|
||||||
### 1. Signal Quality Scoring (`lib/trading/signal-quality.ts`)
|
### 1. Phantom Trade Auto-Closure System
|
||||||
|
**Purpose:** Automatically close positions when size mismatch detected (position opened but wrong size)
|
||||||
|
|
||||||
|
**When triggered:**
|
||||||
|
- Position opened on Drift successfully
|
||||||
|
- Expected size: $50 (50% @ 1x leverage)
|
||||||
|
- Actual size: $1.37 (7% fill - likely oracle price stale or exchange rejection)
|
||||||
|
- Size ratio < 50% threshold → phantom detected
|
||||||
|
|
||||||
|
**Automated response (all happens in <1 second):**
|
||||||
|
1. **Immediate closure:** Market order closes 100% of phantom position
|
||||||
|
2. **Database logging:** Creates trade record with `status='phantom'`, saves P&L
|
||||||
|
3. **n8n notification:** Returns HTTP 200 with full details (not 500 - allows workflow to continue)
|
||||||
|
4. **Telegram alert:** Message includes entry/exit prices, P&L, reason, transaction IDs
|
||||||
|
|
||||||
|
**Why auto-close instead of manual intervention:**
|
||||||
|
- User may be asleep, away from devices, unavailable for hours
|
||||||
|
- Unmonitored position = unlimited risk exposure
|
||||||
|
- Position Manager won't track phantom (by design)
|
||||||
|
- No TP/SL protection, no trailing stop, no monitoring
|
||||||
|
- Better to exit with small loss/gain than leave position exposed
|
||||||
|
- Re-entry always possible if setup was actually good
|
||||||
|
|
||||||
|
**Example notification:**
|
||||||
|
```
|
||||||
|
⚠️ PHANTOM TRADE AUTO-CLOSED
|
||||||
|
|
||||||
|
Symbol: SOL-PERP
|
||||||
|
Direction: LONG
|
||||||
|
Expected Size: $48.75
|
||||||
|
Actual Size: $1.37 (2.8%)
|
||||||
|
|
||||||
|
Entry: $168.50
|
||||||
|
Exit: $168.45
|
||||||
|
P&L: -$0.02
|
||||||
|
|
||||||
|
Reason: Size mismatch detected - likely oracle price issue or exchange rejection
|
||||||
|
Action: Position auto-closed for safety (unmonitored positions = risk)
|
||||||
|
|
||||||
|
TX: 5Yx2Fm8vQHKLdPaw...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database tracking:**
|
||||||
|
- `status='phantom'` field identifies these trades
|
||||||
|
- `isPhantom=true`, `phantomReason='ORACLE_PRICE_MISMATCH'`
|
||||||
|
- `expectedSizeUSD`, `actualSizeUSD` fields for analysis
|
||||||
|
- Exit reason: `'manual'` (phantom auto-close category)
|
||||||
|
- Enables post-trade analysis of phantom frequency and patterns
|
||||||
|
|
||||||
|
**Code location:** `app/api/trading/execute/route.ts` lines 322-445
|
||||||
|
|
||||||
|
### 2. Signal Quality Scoring (`lib/trading/signal-quality.ts`)
|
||||||
**Purpose:** Unified quality validation system that scores trading signals 0-100 based on 5 market metrics
|
**Purpose:** Unified quality validation system that scores trading signals 0-100 based on 5 market metrics
|
||||||
|
|
||||||
**Timeframe-aware thresholds:**
|
**Timeframe-aware thresholds:**
|
||||||
@@ -991,6 +1104,40 @@ trade.realizedPnL += actualRealizedPnL // NOT: result.realizedPnL from SDK
|
|||||||
- **Impact:** 99% of transient DNS failures now auto-recover, preventing missed trades
|
- **Impact:** 99% of transient DNS failures now auto-recover, preventing missed trades
|
||||||
- **Documentation:** See `docs/DNS_RETRY_LOGIC.md` for monitoring queries and metrics
|
- **Documentation:** See `docs/DNS_RETRY_LOGIC.md` for monitoring queries and metrics
|
||||||
|
|
||||||
|
29. **Declaring fixes "working" before deployment (CRITICAL - Nov 13, 2025):**
|
||||||
|
- **Symptom:** AI says "position is protected" or "fix is deployed" when container still running old code
|
||||||
|
- **Root Cause:** Conflating "code committed to git" with "code running in production"
|
||||||
|
- **Real Incident:** Database-first fix committed 15:56, declared "working" at 19:42, but container started 15:06 (old code)
|
||||||
|
- **Result:** Unprotected position opened, database save failed silently, Position Manager never tracked it
|
||||||
|
- **Financial Impact:** User discovered $250+ unprotected position 3.5 hours after opening
|
||||||
|
- **Verification Required:**
|
||||||
|
```bash
|
||||||
|
# ALWAYS check before declaring fix deployed:
|
||||||
|
docker logs trading-bot-v4 | grep "Server starting" | head -1
|
||||||
|
# Compare container start time to git commit timestamp
|
||||||
|
# If container older: FIX NOT DEPLOYED
|
||||||
|
```
|
||||||
|
- **Rule:** NEVER say "fixed", "working", "protected", or "deployed" without verifying container restart timestamp
|
||||||
|
- **Impact:** This is a REAL MONEY system - premature declarations cause financial losses
|
||||||
|
- **Documentation:** Added mandatory deployment verification to VERIFICATION MANDATE section
|
||||||
|
|
||||||
|
30. **Phantom trade notification workflow breaks (Nov 14, 2025):**
|
||||||
|
- **Symptom:** Phantom trade detected, position opened on Drift, but n8n workflow stops with HTTP 500 error. User NOT notified.
|
||||||
|
- **Root Cause:** Execute endpoint returned HTTP 500 when phantom detected, causing n8n chain to halt before Telegram notification
|
||||||
|
- **Problem:** Unmonitored phantom position on exchange while user is asleep/away = unlimited risk exposure
|
||||||
|
- **Fix:** Auto-close phantom trades immediately + return HTTP 200 with warning (allows n8n to continue)
|
||||||
|
```typescript
|
||||||
|
// When phantom detected in app/api/trading/execute/route.ts:
|
||||||
|
// 1. Immediately close position via closePosition()
|
||||||
|
// 2. Save to database (create trade + update with exit info)
|
||||||
|
// 3. Return HTTP 200 with full notification message in response
|
||||||
|
// 4. n8n workflow continues to Telegram notification step
|
||||||
|
```
|
||||||
|
- **Response format change:** `{ success: true, warning: 'Phantom trade detected and auto-closed', isPhantom: true, message: '[Full notification text]', phantomDetails: {...} }`
|
||||||
|
- **Why auto-close:** User can't always respond (sleeping, no phone, traveling). Better to exit with small loss/gain than leave unmonitored position exposed.
|
||||||
|
- **Impact:** Protects user from unlimited risk during unavailable hours. Phantom trades are rare edge cases (oracle issues, exchange rejections).
|
||||||
|
- **Database tracking:** `status='phantom'`, `exitReason='manual'`, enables analysis of phantom frequency and patterns
|
||||||
|
|
||||||
## File Conventions
|
## File Conventions
|
||||||
|
|
||||||
- **API routes:** `app/api/[feature]/[action]/route.ts` (Next.js 15 App Router)
|
- **API routes:** `app/api/[feature]/[action]/route.ts` (Next.js 15 App Router)
|
||||||
@@ -1118,6 +1265,12 @@ if (!enabled) {
|
|||||||
- Never assume SDK data format - log raw values to verify
|
- Never assume SDK data format - log raw values to verify
|
||||||
- SQL query with manual calculation to compare results
|
- SQL query with manual calculation to compare results
|
||||||
- Test boundary cases: 0%, 100%, min/max values
|
- Test boundary cases: 0%, 100%, min/max values
|
||||||
|
11. **DEPLOYMENT VERIFICATION (MANDATORY):** Before declaring ANY fix working:
|
||||||
|
- Check container start time vs commit timestamp
|
||||||
|
- If container older than commit: CODE NOT DEPLOYED
|
||||||
|
- Restart container and verify new code is running
|
||||||
|
- Never say "fixed" or "protected" without deployment confirmation
|
||||||
|
- This is a REAL MONEY system - unverified fixes cause losses
|
||||||
|
|
||||||
## Development Roadmap
|
## Development Roadmap
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { initializeDriftService } from '@/lib/drift/client'
|
import { initializeDriftService } from '@/lib/drift/client'
|
||||||
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
import { openPosition, placeExitOrders, closePosition } from '@/lib/drift/orders'
|
||||||
import { normalizeTradingViewSymbol } from '@/config/trading'
|
import { normalizeTradingViewSymbol } from '@/config/trading'
|
||||||
import { getMergedConfig } from '@/config/trading'
|
import { getMergedConfig } from '@/config/trading'
|
||||||
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||||
@@ -320,11 +320,42 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
|
|
||||||
// CRITICAL: Check for phantom trade (position opened but size mismatch)
|
// CRITICAL: Check for phantom trade (position opened but size mismatch)
|
||||||
if (openResult.isPhantom) {
|
if (openResult.isPhantom) {
|
||||||
console.error(`🚨 PHANTOM TRADE DETECTED - Not adding to Position Manager`)
|
console.error(`🚨 PHANTOM TRADE DETECTED - Auto-closing for safety`)
|
||||||
console.error(` Expected: $${positionSizeUSD.toFixed(2)}`)
|
console.error(` Expected: $${positionSizeUSD.toFixed(2)}`)
|
||||||
console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`)
|
console.error(` Actual: $${openResult.actualSizeUSD?.toFixed(2)}`)
|
||||||
|
|
||||||
|
// IMMEDIATELY close the phantom position (safety first)
|
||||||
|
let closeResult
|
||||||
|
let closedAtPrice = openResult.fillPrice!
|
||||||
|
let closePnL = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`⚠️ Closing phantom position immediately for safety...`)
|
||||||
|
closeResult = await closePosition({
|
||||||
|
symbol: driftSymbol,
|
||||||
|
percentToClose: 100, // Close 100% of whatever size exists
|
||||||
|
slippageTolerance: config.slippageTolerance,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (closeResult.success) {
|
||||||
|
closedAtPrice = closeResult.closePrice || openResult.fillPrice!
|
||||||
|
// Calculate P&L (usually small loss/gain)
|
||||||
|
const priceChange = body.direction === 'long'
|
||||||
|
? ((closedAtPrice - openResult.fillPrice!) / openResult.fillPrice!)
|
||||||
|
: ((openResult.fillPrice! - closedAtPrice) / openResult.fillPrice!)
|
||||||
|
closePnL = (openResult.actualSizeUSD || 0) * priceChange
|
||||||
|
|
||||||
|
console.log(`✅ Phantom position closed at $${closedAtPrice.toFixed(2)}`)
|
||||||
|
console.log(`💰 Phantom P&L: $${closePnL.toFixed(2)}`)
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Failed to close phantom position: ${closeResult.error}`)
|
||||||
|
}
|
||||||
|
} catch (closeError) {
|
||||||
|
console.error(`❌ Error closing phantom position:`, closeError)
|
||||||
|
}
|
||||||
|
|
||||||
// Save phantom trade to database for analysis
|
// Save phantom trade to database for analysis
|
||||||
|
let phantomTradeId: string | undefined
|
||||||
try {
|
try {
|
||||||
const qualityResult = scoreSignalQuality({
|
const qualityResult = scoreSignalQuality({
|
||||||
atr: body.atr || 0,
|
atr: body.atr || 0,
|
||||||
@@ -336,14 +367,15 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
timeframe: body.timeframe,
|
timeframe: body.timeframe,
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTrade({
|
// Create trade record (without exit info initially)
|
||||||
|
const trade = await createTrade({
|
||||||
positionId: openResult.transactionSignature!,
|
positionId: openResult.transactionSignature!,
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
entryPrice: openResult.fillPrice!,
|
entryPrice: openResult.fillPrice!,
|
||||||
positionSizeUSD: positionSizeUSD,
|
positionSizeUSD: openResult.actualSizeUSD || positionSizeUSD,
|
||||||
leverage: leverage, // Use actual symbol-specific leverage
|
leverage: leverage,
|
||||||
stopLossPrice: 0, // Not applicable for phantom
|
stopLossPrice: 0,
|
||||||
takeProfit1Price: 0,
|
takeProfit1Price: 0,
|
||||||
takeProfit2Price: 0,
|
takeProfit2Price: 0,
|
||||||
tp1SizePercent: 0,
|
tp1SizePercent: 0,
|
||||||
@@ -358,27 +390,72 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
volumeAtEntry: body.volumeRatio,
|
volumeAtEntry: body.volumeRatio,
|
||||||
pricePositionAtEntry: body.pricePosition,
|
pricePositionAtEntry: body.pricePosition,
|
||||||
signalQualityScore: qualityResult.score,
|
signalQualityScore: qualityResult.score,
|
||||||
indicatorVersion: body.indicatorVersion || 'v5', // Default to v5 for backward compatibility
|
indicatorVersion: body.indicatorVersion || 'v5',
|
||||||
// Phantom-specific fields
|
|
||||||
status: 'phantom',
|
status: 'phantom',
|
||||||
isPhantom: true,
|
isPhantom: true,
|
||||||
expectedSizeUSD: positionSizeUSD,
|
expectedSizeUSD: positionSizeUSD,
|
||||||
actualSizeUSD: openResult.actualSizeUSD,
|
actualSizeUSD: openResult.actualSizeUSD,
|
||||||
phantomReason: 'ORACLE_PRICE_MISMATCH', // Likely cause based on logs
|
phantomReason: 'ORACLE_PRICE_MISMATCH',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
phantomTradeId = trade.id
|
||||||
console.log(`💾 Phantom trade saved to database for analysis`)
|
console.log(`💾 Phantom trade saved to database for analysis`)
|
||||||
|
|
||||||
|
// If close succeeded, update with exit info
|
||||||
|
if (closeResult?.success) {
|
||||||
|
await updateTradeExit({
|
||||||
|
positionId: openResult.transactionSignature!,
|
||||||
|
exitPrice: closedAtPrice,
|
||||||
|
exitReason: 'manual', // Phantom auto-close (manual category)
|
||||||
|
realizedPnL: closePnL,
|
||||||
|
exitOrderTx: closeResult.transactionSignature || 'PHANTOM_CLOSE',
|
||||||
|
holdTimeSeconds: 0, // Phantom trades close immediately
|
||||||
|
maxDrawdown: Math.abs(Math.min(0, closePnL)),
|
||||||
|
maxGain: Math.max(0, closePnL),
|
||||||
|
maxFavorableExcursion: Math.max(0, closePnL),
|
||||||
|
maxAdverseExcursion: Math.min(0, closePnL),
|
||||||
|
})
|
||||||
|
console.log(`💾 Phantom exit info updated in database`)
|
||||||
|
}
|
||||||
|
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.error('❌ Failed to save phantom trade:', dbError)
|
console.error('❌ Failed to save phantom trade:', dbError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare notification message for n8n to send via Telegram
|
||||||
|
const phantomNotification =
|
||||||
|
`⚠️ PHANTOM TRADE AUTO-CLOSED\n\n` +
|
||||||
|
`Symbol: ${driftSymbol}\n` +
|
||||||
|
`Direction: ${body.direction.toUpperCase()}\n` +
|
||||||
|
`Expected Size: $${positionSizeUSD.toFixed(2)}\n` +
|
||||||
|
`Actual Size: $${(openResult.actualSizeUSD || 0).toFixed(2)} (${((openResult.actualSizeUSD || 0) / positionSizeUSD * 100).toFixed(1)}%)\n\n` +
|
||||||
|
`Entry: $${openResult.fillPrice!.toFixed(2)}\n` +
|
||||||
|
`Exit: $${closedAtPrice.toFixed(2)}\n` +
|
||||||
|
`P&L: $${closePnL.toFixed(2)}\n\n` +
|
||||||
|
`Reason: Size mismatch detected - likely oracle price issue or exchange rejection\n` +
|
||||||
|
`Action: Position auto-closed for safety (unmonitored positions = risk)\n\n` +
|
||||||
|
`TX: ${openResult.transactionSignature?.slice(0, 20)}...`
|
||||||
|
|
||||||
|
console.log(`📱 Phantom notification prepared:`, phantomNotification)
|
||||||
|
|
||||||
|
// Return HTTP 200 with warning (not 500) so n8n workflow continues to notification
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: true, // Changed from false - position was handled safely
|
||||||
error: 'Phantom trade detected',
|
warning: 'Phantom trade detected and auto-closed',
|
||||||
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.`,
|
isPhantom: true,
|
||||||
|
message: phantomNotification, // Full notification message for n8n
|
||||||
|
phantomDetails: {
|
||||||
|
expectedSize: positionSizeUSD,
|
||||||
|
actualSize: openResult.actualSizeUSD,
|
||||||
|
sizeRatio: (openResult.actualSizeUSD || 0) / positionSizeUSD,
|
||||||
|
autoClosed: closeResult?.success || false,
|
||||||
|
pnl: closePnL,
|
||||||
|
entryTx: openResult.transactionSignature,
|
||||||
|
exitTx: closeResult?.transactionSignature,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 200 } // Changed from 500 - allows n8n to continue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = {
|
|||||||
trailingStopActivation: 0.5, // Activate trailing when runner is +0.5% in profit
|
trailingStopActivation: 0.5, // Activate trailing when runner is +0.5% in profit
|
||||||
|
|
||||||
// Signal Quality
|
// Signal Quality
|
||||||
minSignalQualityScore: 65, // Minimum quality score for initial entry (raised from 60)
|
minSignalQualityScore: 60, // Minimum quality score for initial entry (lowered from 65 on Nov 12, 2025 - data showed 60-64 tier outperformed)
|
||||||
|
|
||||||
// Position Scaling (conservative defaults)
|
// Position Scaling (conservative defaults)
|
||||||
enablePositionScaling: false, // Disabled by default - enable after testing
|
enablePositionScaling: false, // Disabled by default - enable after testing
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: trading-bot-v4
|
container_name: trading-bot-v4
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
dns:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
ports:
|
ports:
|
||||||
- "3001:3000"
|
- "3001:3000"
|
||||||
environment:
|
environment:
|
||||||
@@ -80,6 +83,14 @@ services:
|
|||||||
# ================================
|
# ================================
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
command: |
|
||||||
|
postgres
|
||||||
|
-c shared_buffers=128MB
|
||||||
|
-c effective_cache_size=512MB
|
||||||
|
-c work_mem=8MB
|
||||||
|
-c maintenance_work_mem=64MB
|
||||||
|
-c max_connections=20
|
||||||
|
-c random_page_cost=1.1
|
||||||
container_name: trading-bot-postgres
|
container_name: trading-bot-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
121
docker-compose.yml.backup-20251113-002225
Normal file
121
docker-compose.yml.backup-20251113-002225
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Trading Bot v4 - Docker Compose Configuration
|
||||||
|
# Production-ready setup with PostgreSQL and monitoring
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ================================
|
||||||
|
# Trading Bot Application
|
||||||
|
# ================================
|
||||||
|
trading-bot:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: trading-bot-v4
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
environment:
|
||||||
|
# Node environment
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3000
|
||||||
|
|
||||||
|
# Load from .env file (create from .env.example)
|
||||||
|
DRIFT_WALLET_PRIVATE_KEY: ${DRIFT_WALLET_PRIVATE_KEY}
|
||||||
|
DRIFT_ENV: ${DRIFT_ENV:-mainnet-beta}
|
||||||
|
API_SECRET_KEY: ${API_SECRET_KEY}
|
||||||
|
SOLANA_RPC_URL: ${SOLANA_RPC_URL}
|
||||||
|
PYTH_HERMES_URL: ${PYTH_HERMES_URL:-https://hermes.pyth.network}
|
||||||
|
|
||||||
|
# Trading configuration
|
||||||
|
MAX_POSITION_SIZE_USD: ${MAX_POSITION_SIZE_USD:-50}
|
||||||
|
LEVERAGE: ${LEVERAGE:-10}
|
||||||
|
STOP_LOSS_PERCENT: ${STOP_LOSS_PERCENT:--1.5}
|
||||||
|
TAKE_PROFIT_1_PERCENT: ${TAKE_PROFIT_1_PERCENT:-0.7}
|
||||||
|
TAKE_PROFIT_1_SIZE_PERCENT: ${TAKE_PROFIT_1_SIZE_PERCENT:-50}
|
||||||
|
TAKE_PROFIT_2_PERCENT: ${TAKE_PROFIT_2_PERCENT:-1.5}
|
||||||
|
TAKE_PROFIT_2_SIZE_PERCENT: ${TAKE_PROFIT_2_SIZE_PERCENT:-50}
|
||||||
|
|
||||||
|
# Database (if using PostgreSQL)
|
||||||
|
DATABASE_URL: ${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/trading_bot_v4}
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||||
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
||||||
|
DISCORD_WEBHOOK_URL: ${DISCORD_WEBHOOK_URL:-}
|
||||||
|
|
||||||
|
# n8n integration
|
||||||
|
N8N_WEBHOOK_URL: ${N8N_WEBHOOK_URL:-}
|
||||||
|
TRADINGVIEW_WEBHOOK_SECRET: ${TRADINGVIEW_WEBHOOK_SECRET:-}
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||||
|
DRY_RUN: ${DRY_RUN:-false}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Mount .env file for settings persistence
|
||||||
|
- ./.env:/app/.env
|
||||||
|
|
||||||
|
# Mount logs directory
|
||||||
|
- ./logs:/app/logs
|
||||||
|
|
||||||
|
# Mount Docker socket for container restart capability
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
# Mount for hot reload in development (comment out in production)
|
||||||
|
# - ./v4:/app/v4:ro
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- trading-net
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# PostgreSQL Database (Optional)
|
||||||
|
# ================================
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: trading-bot-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: trading_bot_v4
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8"
|
||||||
|
volumes:
|
||||||
|
# Persist database data
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
# Custom initialization scripts (optional)
|
||||||
|
- ./prisma/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||||
|
networks:
|
||||||
|
- trading-net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Networks
|
||||||
|
# ================================
|
||||||
|
networks:
|
||||||
|
trading-net:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.28.0.0/16
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Volumes
|
||||||
|
# ================================
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
driver: local
|
||||||
@@ -102,8 +102,12 @@ export class DriftService {
|
|||||||
error?.code === 'EAI_AGAIN' ||
|
error?.code === 'EAI_AGAIN' ||
|
||||||
error?.cause?.code === 'EAI_AGAIN'
|
error?.cause?.code === 'EAI_AGAIN'
|
||||||
|
|
||||||
|
console.log(`🔍 Error detection: isTransient=${isTransient}, attempt=${attempt}/${maxRetries}`)
|
||||||
|
console.log(`🔍 Error details: message="${error?.message}", code="${error?.code}", cause.code="${error?.cause?.code}"`)
|
||||||
|
|
||||||
if (!isTransient || attempt === maxRetries) {
|
if (!isTransient || attempt === maxRetries) {
|
||||||
// Non-transient error or max retries reached - fail immediately
|
// Non-transient error or max retries reached - fail immediately
|
||||||
|
console.log(`❌ Not retrying: isTransient=${isTransient}, maxed=${attempt === maxRetries}`)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user