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:
mindesbunister
2025-11-14 05:37:51 +01:00
parent 4ad509928f
commit 6590f4fb1e
7 changed files with 383 additions and 17 deletions

4
.env
View File

@@ -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

View File

@@ -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

View File

@@ -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
) )
} }

View File

@@ -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

View File

@@ -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:

View 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

View File

@@ -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
} }