Major additions: - Exit strategy details: 3-tier scaling (TP1 75%, TP2 80% of remaining, 5% runner with trailing stop) - Signal quality system: 5 metrics scored 0-100, filters trades at 60+ threshold - Runner implementation: Trailing stop activation after TP2, peakPrice tracking - Database fields: signalQualityScore, MAE/MFE, configSnapshot for state persistence - New API endpoints: /check-risk, /analytics/last-trade, /restart - Updated workflows with quality score validation and runner management - Common pitfalls: Quality score duplication, runner configuration confusion - Development roadmap: Link to POSITION_SCALING_ROADMAP.md with 6 phases Critical corrections: - Position Manager singleton: getPositionManager() → getInitializedPositionManager() - Updated monitoring loop details with external closure detection and state saving
13 KiB
AI Agent Instructions for Trading Bot v4
Architecture Overview
Type: Autonomous cryptocurrency trading bot with Next.js 15 frontend + Solana/Drift Protocol backend
Data Flow: TradingView → n8n webhook → Next.js API → Drift Protocol (Solana DEX) → Real-time monitoring → Auto-exit
Key Design Principle: Dual-layer redundancy - every trade has both on-chain orders (Drift) AND software monitoring (Position Manager) as backup.
Exit Strategy: Three-tier scaling system:
- TP1 at +1.5%: Close 75% (configurable via
TAKE_PROFIT_1_SIZE_PERCENT) - TP2 at +3.0%: Close 80% of remaining = 20% total (configurable via
TAKE_PROFIT_2_SIZE_PERCENT) - Runner: 5% remaining with 0.3% trailing stop (configurable via
TRAILING_STOP_PERCENT)
Signal Quality System: Filters trades based on 5 metrics (ATR, ADX, RSI, volumeRatio, pricePosition) scored 0-100. Only trades scoring 60+ are executed. Scores stored in database for future optimization.
Critical Components
1. Position Manager (lib/trading/position-manager.ts)
Purpose: Software-based monitoring loop that checks prices every 2 seconds and closes positions via market orders
Singleton pattern: Always use getInitializedPositionManager() - never instantiate directly
const positionManager = await getInitializedPositionManager()
await positionManager.addTrade(activeTrade)
Key behaviors:
- Tracks
ActiveTradeobjects in a Map - Three-tier exits: TP1 (75%), TP2 (80% of remaining), Runner (with trailing stop)
- Dynamic SL adjustments: Moves to breakeven after TP1, locks profit at +1.2%
- Trailing stop: Activates after TP2, tracks
peakPriceand trails by configured % - Closes positions via
closePosition()market orders when targets hit - Acts as backup if on-chain orders don't fill
- State persistence: Saves to database, restores on restart via
configSnapshot.positionManagerState
2. Drift Client (lib/drift/client.ts)
Purpose: Solana/Drift Protocol SDK wrapper for order execution
Singleton pattern: Use initializeDriftService() and getDriftService() - maintains single connection
const driftService = await initializeDriftService()
const health = await driftService.getAccountHealth()
Wallet handling: Supports both JSON array [91,24,...] and base58 string formats from Phantom wallet
3. Order Placement (lib/drift/orders.ts)
Critical function: placeExitOrders() - places TP/SL orders on-chain
Dual Stop System (USE_DUAL_STOPS=true):
// Soft stop: TRIGGER_LIMIT at -1.5% (avoids wicks)
// Hard stop: TRIGGER_MARKET at -2.5% (guarantees exit)
Order types:
- Entry: MARKET (immediate execution)
- TP1/TP2: LIMIT reduce-only orders
- Soft SL: TRIGGER_LIMIT reduce-only
- Hard SL: TRIGGER_MARKET reduce-only
4. Database (lib/database/trades.ts + prisma/schema.prisma)
Purpose: PostgreSQL via Prisma ORM for trade history and analytics
Models: Trade, PriceUpdate, SystemEvent, DailyStats
Singleton pattern: Use getPrismaClient() - never instantiate PrismaClient directly
Key functions:
createTrade()- Save trade after execution (includes dual stop TX signatures + signalQualityScore)updateTradeExit()- Record exit with P&LaddPriceUpdate()- Track price movements (called by Position Manager)getTradeStats()- Win rate, profit factor, avg win/lossgetLastTrade()- Fetch most recent trade for analytics dashboard
Important fields:
signalQualityScore(Int?) - 0-100 score for data-driven optimizationmaxFavorableExcursion/maxAdverseExcursion- Track best/worst P&L during trade lifetimeconfigSnapshot(Json) - Stores Position Manager state for crash recoveryatr,adx,rsi,volumeRatio,pricePosition- Context metrics from TradingView
Configuration System
Three-layer merge:
DEFAULT_TRADING_CONFIG(config/trading.ts)- Environment variables (.env) via
getConfigFromEnv() - Runtime overrides via
getMergedConfig(overrides)
Always use: getMergedConfig() to get final config - never read env vars directly in business logic
Symbol normalization: TradingView sends "SOLUSDT" → must convert to "SOL-PERP" for Drift
const driftSymbol = normalizeTradingViewSymbol(body.symbol)
API Endpoints Architecture
Authentication: All /api/trading/* endpoints (except /test) require Authorization: Bearer API_SECRET_KEY
Pattern: Each endpoint follows same flow:
- Auth check
- Get config via
getMergedConfig() - Initialize Drift service
- Check account health
- Execute operation
- Save to database
- Add to Position Manager if applicable
Key endpoints:
/api/trading/execute- Main entry point from n8n (production, requires auth)/api/trading/check-risk- Pre-execution validation (duplicate check, quality score, rate limits)/api/trading/test- Test trades from settings UI (no auth required)/api/trading/close- Manual position closing/api/trading/positions- Query open positions from Drift/api/settings- Get/update config (writes to .env file)/api/analytics/last-trade- Fetch most recent trade details for dashboard/api/restart- Create restart flag for watch-restart.sh script
Critical Workflows
Execute Trade (Production)
TradingView alert → n8n Parse Signal Enhanced (extracts metrics)
↓ /api/trading/check-risk [validates quality score ≥60, checks duplicates]
↓ /api/trading/execute
↓ normalize symbol (SOLUSDT → SOL-PERP)
↓ getMergedConfig()
↓ openPosition() [MARKET order]
↓ calculate dual stop prices if enabled
↓ placeExitOrders() [on-chain TP1/TP2/SL orders]
↓ calculateQualityScore() [compute 0-100 score from metrics]
↓ createTrade() [save to database with signalQualityScore]
↓ positionManager.addTrade() [start monitoring]
Position Monitoring Loop
Position Manager every 2s:
↓ Verify on-chain position still exists (detect external closures)
↓ getPythPriceMonitor().getLatestPrice()
↓ Calculate current P&L and update peakPrice
↓ Check emergency stop (-2%) → closePosition(100%)
↓ Check SL hit → closePosition(100%)
↓ Check TP1 hit → closePosition(75%), move SL to breakeven
↓ Check profit lock trigger (+1.2%) → move SL to +configured%
↓ Check TP2 hit → closePosition(80% of remaining), activate runner
↓ Check trailing stop (if runner active) → adjust SL dynamically based on peakPrice
↓ addPriceUpdate() [save to database every N checks]
↓ saveTradeState() [persist Position Manager state for crash recovery]
Settings Update
Web UI → /api/settings POST
↓ Validate new settings
↓ Write to .env file using string replacement
↓ Return success
↓ User clicks "Restart Bot" → /api/restart
↓ Creates /tmp/trading-bot-restart.flag
↓ watch-restart.sh detects flag
↓ Executes: docker restart trading-bot-v4
Docker Context
Multi-stage build: deps → builder → runner (Node 20 Alpine)
Critical Dockerfile steps:
- Install deps with
npm install --production - Copy source and
npx prisma generate(MUST happen before build) npm run build(Next.js standalone output)- Runner stage copies standalone + static + node_modules + Prisma client
Container networking:
- External:
trading-bot-v4on port 3001 - Internal: Next.js on port 3000
- Database:
trading-bot-postgreson 172.28.0.0/16 network
DATABASE_URL caveat: Use trading-bot-postgres (container name) in .env for runtime, but localhost:5432 for Prisma CLI migrations from host
Project-Specific Patterns
1. Singleton Services
Never create multiple instances - always use getter functions:
const driftService = await initializeDriftService() // NOT: new DriftService()
const positionManager = getPositionManager() // NOT: new PositionManager()
const prisma = getPrismaClient() // NOT: new PrismaClient()
2. Price Calculations
Direction matters for long vs short:
function calculatePrice(entry: number, percent: number, direction: 'long' | 'short') {
if (direction === 'long') {
return entry * (1 + percent / 100) // Long: +1% = higher price
} else {
return entry * (1 - percent / 100) // Short: +1% = lower price
}
}
3. Error Handling
Database failures should not fail trades - always wrap in try/catch:
try {
await createTrade(params)
console.log('💾 Trade saved to database')
} catch (dbError) {
console.error('❌ Failed to save trade:', dbError)
// Don't fail the trade if database save fails
}
4. Reduce-Only Orders
All exit orders MUST be reduce-only (can only close, not open positions):
const orderParams = {
reduceOnly: true, // CRITICAL for TP/SL orders
// ... other params
}
Testing Commands
# Local development
npm run dev
# Build production
npm run build && npm start
# Docker build and restart
docker compose build trading-bot
docker compose up -d --force-recreate trading-bot
docker logs -f trading-bot-v4
# Database operations
npx prisma generate # Generate client
DATABASE_URL="postgresql://...@localhost:5432/..." npx prisma migrate dev
docker exec trading-bot-postgres psql -U postgres -d trading_bot_v4 -c "\dt"
# Test trade from UI
# Go to http://localhost:3001/settings
# Click "Test LONG" or "Test SHORT"
Common Pitfalls
-
Prisma not generated in Docker: Must run
npx prisma generatein Dockerfile BEFOREnpm run build -
Wrong DATABASE_URL: Container runtime needs
trading-bot-postgres, Prisma CLI from host needslocalhost:5432 -
Symbol format mismatch: Always normalize with
normalizeTradingViewSymbol()before calling Drift -
Missing reduce-only flag: Exit orders without
reduceOnly: truecan accidentally open new positions -
Singleton violations: Creating multiple DriftClient or Position Manager instances causes connection/state issues
-
Type errors with Prisma: The Trade type from Prisma is only available AFTER
npx prisma generate- use explicit types or// @ts-ignorecarefully -
Quality score duplication: Signal quality calculation exists in BOTH
check-riskandexecuteendpoints - keep logic synchronized -
Runner configuration confusion:
TAKE_PROFIT_1_SIZE_PERCENT=75means "close 75% at TP1" (not "keep 75%")TAKE_PROFIT_2_SIZE_PERCENT=80means "close 80% of REMAINING" (not of original)- Actual runner size = (100 - TP1%) × (100 - TP2%) / 100 = 5% with defaults
File Conventions
- API routes:
app/api/[feature]/[action]/route.ts(Next.js 15 App Router) - Services:
lib/[service]/[module].ts(drift, pyth, trading, database) - Config: Single source in
config/trading.tswith env merging - Types: Define interfaces in same file as implementation (not separate types directory)
- Console logs: Use emojis for visual scanning: 🎯 🚀 ✅ ❌ 💰 📊 🛡️
When Making Changes
- Adding new config: Update DEFAULT_TRADING_CONFIG + getConfigFromEnv() + .env file
- Adding database fields: Update prisma/schema.prisma →
npx prisma migrate dev→npx prisma generate→ rebuild Docker - Changing order logic: Test with DRY_RUN=true first, use small position sizes ($10)
- API endpoint changes: Update both endpoint + corresponding n8n workflow JSON (Check Risk and Execute Trade nodes)
- Docker changes: Rebuild with
docker compose build trading-botthen restart container - Modifying quality score logic: Update BOTH
/api/trading/check-riskand/api/trading/executeendpoints - Exit strategy changes: Modify Position Manager logic + update on-chain order placement in
placeExitOrders()
Development Roadmap
See POSITION_SCALING_ROADMAP.md for planned optimizations:
- Phase 1 (CURRENT): Collect data with quality scores (20-50 trades needed)
- Phase 2: ATR-based dynamic targets (adapt to volatility)
- Phase 3: Signal quality-based scaling (high quality = larger runners)
- Phase 4: Direction-based optimization (shorts vs longs have different performance)
- Phase 5: Optimize runner size (5% → 10-25%) and trailing stop (0.3% fixed → ATR-based)
- Phase 6: ML-based exit prediction (future)
Data-driven approach: Each phase requires validation through SQL analysis before implementation. No premature optimization.
Integration Points
- n8n: Expects exact response format from
/api/trading/execute(see n8n-complete-workflow.json) - Drift Protocol: Uses SDK v2.75.0 - check docs at docs.drift.trade for API changes
- Pyth Network: WebSocket + HTTP fallback for price feeds (handles reconnection)
- PostgreSQL: Version 16-alpine, must be running before bot starts
Key Mental Model: Think of this as two parallel systems (on-chain orders + software monitoring) working together. The Position Manager is the "backup brain" that constantly watches and acts if on-chain orders fail. Both write to the same database for complete trade history.