Files
trading_bot_v4/.github/copilot-instructions.md
mindesbunister 2e47731e8e Update AI instructions with latest fixes
Added sections:
- Recent Critical Fixes (2024-11-10): Runner system + anti-chop filter V2
- JavaScript || vs ?? operator gotcha (#21)
- Range-bound chop detection details (#22)
- Updated anti-chop filter description with backtest results
2025-11-10 15:43:48 +01:00

572 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:** TP2-as-Runner system (CURRENT):
- TP1 at +0.4%: Close 75% (configurable via `TAKE_PROFIT_1_SIZE_PERCENT`)
- TP2 at +0.7%: **Activates trailing stop** on full 25% remaining (no position close)
- Runner: 25% remaining with ATR-based trailing stop (5x larger than old 5% system)
**Per-Symbol Configuration:** SOL and ETH have independent enable/disable toggles and position sizing:
- `SOLANA_ENABLED`, `SOLANA_POSITION_SIZE`, `SOLANA_LEVERAGE` (defaults: true, $210, 10x)
- `ETHEREUM_ENABLED`, `ETHEREUM_POSITION_SIZE`, `ETHEREUM_LEVERAGE` (defaults: true, $4, 1x)
- BTC and other symbols fall back to global settings (`MAX_POSITION_SIZE_USD`, `LEVERAGE`)
- **Priority:** Per-symbol ENV → Market config → Global ENV → Defaults
**Signal Quality System:** Filters trades based on 5 metrics (ATR, ADX, RSI, volumeRatio, pricePosition) scored 0-100. Minimum score threshold configurable via `MIN_SIGNAL_QUALITY_SCORE` env var (default: 65, editable via settings page). Scores stored in database for future optimization.
**Timeframe-Aware Scoring:** Signal quality thresholds adjust based on timeframe (5min vs daily):
- 5min: ADX 12+ trending (vs 18+ for daily), ATR 0.2-0.7% healthy (vs 0.4%+ for daily)
- Anti-chop filter: -20 points for extreme sideways (ADX <10, ATR <0.25%, Vol <0.9x)
- Pass `timeframe` param to `scoreSignalQuality()` from TradingView alerts (e.g., `timeframe: "5"`)
**MAE/MFE Tracking:** Every trade tracks Maximum Favorable Excursion (best profit %) and Maximum Adverse Excursion (worst loss %) updated every 2s. Used for data-driven optimization of TP/SL levels.
**Manual Trading via Telegram:** Send plain-text messages like `long sol`, `short eth`, `long btc` to open positions instantly (bypasses n8n, calls `/api/trading/execute` directly with preset healthy metrics).
## Recent Critical Fixes (2024-11-10)
### Runner System - Three Cascading Bugs Fixed
The TP2-as-runner feature was broken by three separate bugs:
1. **P&L Calculation Bug (65x inflation)** - `lib/drift/orders.ts`, `lib/trading/position-manager.ts`
- Calculated P&L on notional ($2,100) instead of collateral ($210)
- Database showed +$1,345, reality was -$806 loss
- Fix: `collateralUSD = notional / leverage`, calculate P&L on collateral
2. **Post-TP1 Logic Bug** - `lib/trading/position-manager.ts` lines 1010-1030
- Placed TP order at TP2 price after TP1 hit (closed position instead of trailing)
- Fix: Check `if (config.takeProfit2SizePercent === 0)` to skip TP orders
3. **JavaScript || Operator Bug** - `app/api/trading/execute/route.ts`, `test/route.ts`
- `config.takeProfit2SizePercent || 100` treated 0 as falsy → returned 100
- Fix: Use `??` (nullish coalescing) instead of `||` for numeric defaults
### Anti-Chop Filter V2 - Range-Bound Detection
- **Problem:** Flip-flop trades in sideways markets (stopped out in 8-24 seconds)
- **Fix:** -25 points when price position <40% AND ADX <25 (both conditions)
- **Location:** `lib/trading/signal-quality.ts` lines 145-165
- **Impact:** Win rate 43.8% → 55.6%, profit per trade +86%
- **Backtest:** Would have blocked all 3 flip-flop trades from today
## Critical Components
### 1. Signal Quality Scoring (`lib/trading/signal-quality.ts`)
**Purpose:** Unified quality validation system that scores trading signals 0-100 based on 5 market metrics
**Timeframe-aware thresholds:**
```typescript
scoreSignalQuality({
atr, adx, rsi, volumeRatio, pricePosition,
timeframe?: string // "5" for 5min, undefined for higher timeframes
})
```
**5min chart adjustments:**
- ADX healthy range: 12-22 (vs 18-30 for daily)
- ATR healthy range: 0.2-0.7% (vs 0.4%+ for daily)
- Anti-chop filter: -20 points for extreme sideways (ADX <10, ATR <0.25%, Vol <0.9x)
**Price position penalties (all timeframes):**
- Long at 90-95%+ range: -15 to -30 points (chasing highs)
- Short at <5-10% range: -15 to -30 points (chasing lows)
- **ANTI-CHOP (v2024-11-10):** Price position <40% + ADX <25 = -25 points (RANGE-BOUND CHOP)
- Prevents flip-flop losses from entering range extremes
- Targets sideways markets where price is low in range but trend is weak
- Backtest: 43.8% → 55.6% win rate, 86% higher profit per trade
**Key behaviors:**
- Returns score 0-100 and detailed breakdown object
- Minimum score threshold configurable via `config.minSignalQualityScore` (default: 65)
- Called by both `/api/trading/check-risk` and `/api/trading/execute`
- Scores saved to database for post-trade analysis
### 2. 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
```typescript
const positionManager = await getInitializedPositionManager()
await positionManager.addTrade(activeTrade)
```
**Key behaviors:**
- Tracks `ActiveTrade` objects in a Map
- **TP2-as-Runner system**: TP1 (75%) → TP2 trigger (no close, activate trailing) → 25% runner with ATR-based trailing stop
- Dynamic SL adjustments: Moves to breakeven after TP1, locks profit at +1.2%
- **On-chain order synchronization:** After TP1 hits, calls `cancelAllOrders()` then `placeExitOrders()` with updated SL price at breakeven
- Trailing stop: Activates when TP2 price hit, tracks `peakPrice` and trails by ATR-based %
- Closes positions via `closePosition()` market orders when targets hit
- Acts as backup if on-chain orders don't fill
- State persistence: Saves to database, restores on restart via `configSnapshot.positionManagerState`
- **Grace period for new trades:** Skips "external closure" detection for positions <30 seconds old (Drift positions take 5-10s to propagate)
- **Exit reason detection:** Uses trade state flags (`tp1Hit`, `tp2Hit`) and realized P&L to determine exit reason, NOT current price (avoids misclassification when price moves after order fills)
- **Real P&L calculation:** Calculates actual profit based on entry vs exit price, not SDK's potentially incorrect values
### 3. Telegram Bot (`telegram_command_bot.py`)
**Purpose:** Python-based Telegram bot for manual trading commands and position status monitoring
**Manual trade commands via plain text:**
```python
# User sends plain text message (not slash commands)
"long sol" Opens SOL-PERP long position
"short eth" Opens ETH-PERP short position
"long btc" Opens BTC-PERP long position
```
**Key behaviors:**
- MessageHandler processes all text messages (not just commands)
- Maps user-friendly symbols (sol, eth, btc) to Drift format (SOL-PERP, etc.)
- Calls `/api/trading/execute` directly with preset healthy metrics (ATR=1.0, ADX=25, RSI=50, volumeRatio=1.2)
- Bypasses n8n workflow and TradingView requirements
- 60-second timeout for API calls
- Responds with trade confirmation or error message
**Status command:**
```python
/status Returns JSON of open positions from Drift
```
**Implementation details:**
- Uses `python-telegram-bot` library
- Deployed via `docker-compose.telegram-bot.yml`
- Requires `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHANNEL_ID` in .env
- API calls to `http://trading-bot:3000/api/trading/execute`
**Drift client integration:**
- Singleton pattern: Use `initializeDriftService()` and `getDriftService()` - maintains single connection
```typescript
const driftService = await initializeDriftService()
const health = await driftService.getAccountHealth()
```
- Wallet handling: Supports both JSON array `[91,24,...]` and base58 string formats from Phantom wallet
### 4. Order Placement (`lib/drift/orders.ts`)
**Critical functions:**
- `openPosition()` - Opens market position with transaction confirmation
- `closePosition()` - Closes position with transaction confirmation
- `placeExitOrders()` - Places TP/SL orders on-chain
**CRITICAL: Transaction Confirmation Pattern**
Both `openPosition()` and `closePosition()` MUST confirm transactions on-chain:
```typescript
const txSig = await driftClient.placePerpOrder(orderParams)
console.log('⏳ Confirming transaction on-chain...')
const connection = driftService.getConnection()
const confirmation = await connection.confirmTransaction(txSig, 'confirmed')
if (confirmation.value.err) {
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`)
}
console.log('✅ Transaction confirmed on-chain')
```
Without this, the SDK returns signatures for transactions that never execute, causing phantom trades/closes.
**Dual Stop System** (USE_DUAL_STOPS=true):
```typescript
// 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
### 5. 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&L
- `addPriceUpdate()` - Track price movements (called by Position Manager)
- `getTradeStats()` - Win rate, profit factor, avg win/loss
- `getLastTrade()` - Fetch most recent trade for analytics dashboard
**Important fields:**
- `signalQualityScore` (Int?) - 0-100 score for data-driven optimization
- `signalQualityVersion` (String?) - Tracks which scoring logic was used ('v1', 'v2', 'v3')
- v1: Original logic (price position < 5% threshold)
- v2: Added volume compensation for low ADX (2025-11-07)
- v3: CURRENT - Stricter breakdown requirements: positions < 15% require (ADX > 18 AND volume > 1.2x) OR (RSI < 35 for shorts / RSI > 60 for longs)
- All new trades tagged with current version for comparative analysis
- `maxFavorableExcursion` / `maxAdverseExcursion` - Track best/worst P&L during trade lifetime
- `maxFavorablePrice` / `maxAdversePrice` - Track prices at MFE/MAE points
- `configSnapshot` (Json) - Stores Position Manager state for crash recovery
- `atr`, `adx`, `rsi`, `volumeRatio`, `pricePosition` - Context metrics from TradingView
**Per-symbol functions:**
- `getLastTradeTimeForSymbol(symbol)` - Get last trade time for specific coin (enables per-symbol cooldown)
- Each coin (SOL/ETH/BTC) has independent cooldown timer to avoid missed opportunities
## Configuration System
**Three-layer merge:**
1. `DEFAULT_TRADING_CONFIG` (config/trading.ts)
2. Environment variables (.env) via `getConfigFromEnv()`
3. Runtime overrides via `getMergedConfig(overrides)`
**Always use:** `getMergedConfig()` to get final config - never read env vars directly in business logic
**Per-symbol position sizing:** Use `getPositionSizeForSymbol(symbol, config)` which returns `{ size, leverage, enabled }`
```typescript
const { size, leverage, enabled } = getPositionSizeForSymbol('SOL-PERP', config)
if (!enabled) {
return NextResponse.json({ success: false, error: 'Symbol trading disabled' }, { status: 400 })
}
```
**Symbol normalization:** TradingView sends "SOLUSDT" → must convert to "SOL-PERP" for Drift
```typescript
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:
1. Auth check
2. Get config via `getMergedConfig()`
3. Initialize Drift service
4. Check account health
5. Execute operation
6. Save to database
7. 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, **per-symbol cooldown**, rate limits, **symbol enabled check**)
- `/api/trading/test` - Test trades from settings UI (no auth required, **respects symbol enable/disable**)
- `/api/trading/close` - Manual position closing
- `/api/trading/positions` - Query open positions from Drift
- `/api/settings` - Get/update config (writes to .env file, **includes per-symbol settings**)
- `/api/analytics/last-trade` - Fetch most recent trade details for dashboard (includes quality score)
- `/api/analytics/version-comparison` - Compare performance across signal quality logic versions (v1/v2/v3)
- `/api/restart` - Create restart flag for watch-restart.sh script
## Critical Workflows
### Execute Trade (Production)
```
TradingView alert → n8n Parse Signal Enhanced (extracts metrics + timeframe)
↓ /api/trading/check-risk [validates quality score ≥60, checks duplicates, per-symbol cooldown]
↓ /api/trading/execute
↓ normalize symbol (SOLUSDT → SOL-PERP)
↓ getMergedConfig()
↓ getPositionSizeForSymbol() [check if symbol enabled + get sizing]
↓ openPosition() [MARKET order]
↓ calculate dual stop prices if enabled
↓ placeExitOrders() [on-chain TP1/TP2/SL orders]
↓ scoreSignalQuality({ ..., timeframe }) [compute 0-100 score with timeframe-aware thresholds]
↓ 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 MAE/MFE metrics
↓ Check emergency stop (-2%) → closePosition(100%)
↓ Check SL hit → closePosition(100%)
↓ Check TP1 hit → closePosition(75%), cancelAllOrders(), placeExitOrders() with SL at 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 + MAE/MFE 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:**
1. Install deps with `npm install --production`
2. Copy source and `npx prisma generate` (MUST happen before build)
3. `npm run build` (Next.js standalone output)
4. Runner stage copies standalone + static + node_modules + Prisma client
**Container networking:**
- External: `trading-bot-v4` on port 3001
- Internal: Next.js on port 3000
- Database: `trading-bot-postgres` on 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:
```typescript
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:
```typescript
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:
```typescript
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):
```typescript
const orderParams = {
reduceOnly: true, // CRITICAL for TP/SL orders
// ... other params
}
```
## Testing Commands
```bash
# 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
1. **Prisma not generated in Docker:** Must run `npx prisma generate` in Dockerfile BEFORE `npm run build`
2. **Wrong DATABASE_URL:** Container runtime needs `trading-bot-postgres`, Prisma CLI from host needs `localhost:5432`
3. **Symbol format mismatch:** Always normalize with `normalizeTradingViewSymbol()` before calling Drift (applies to ALL endpoints including `/api/trading/close`)
4. **Missing reduce-only flag:** Exit orders without `reduceOnly: true` can accidentally open new positions
5. **Singleton violations:** Creating multiple DriftClient or Position Manager instances causes connection/state issues
6. **Type errors with Prisma:** The Trade type from Prisma is only available AFTER `npx prisma generate` - use explicit types or `// @ts-ignore` carefully
7. **Hardcoded config values:** NEVER use hardcoded values for configurable settings in API endpoints. Always read from `config.minSignalQualityScore` or similar config properties. Settings changed via the UI won't take effect if endpoints use hardcoded values.
8. **Quality score duplication:** Signal quality calculation exists in BOTH `check-risk` and `execute` endpoints - keep logic synchronized
9. **TP2-as-Runner configuration:**
- `takeProfit2SizePercent: 0` means "TP2 activates trailing stop, no position close"
- This creates 25% runner (vs old 5% system) for better profit capture
- `TAKE_PROFIT_2_PERCENT=0.7` sets TP2 trigger price, `TAKE_PROFIT_2_SIZE_PERCENT` should be 0
- Settings UI correctly shows "TP2 activates trailing stop" instead of size percentage
10. **P&L calculation CRITICAL:** Use actual entry vs exit price calculation, not SDK values:
```typescript
const profitPercent = this.calculateProfitPercent(trade.entryPrice, exitPrice, trade.direction)
const actualRealizedPnL = (closedSizeUSD * profitPercent) / 100
trade.realizedPnL += actualRealizedPnL // NOT: result.realizedPnL from SDK
```
11. **Transaction confirmation CRITICAL:** Both `openPosition()` AND `closePosition()` MUST call `connection.confirmTransaction()` after `placePerpOrder()`. Without this, the SDK returns transaction signatures that aren't confirmed on-chain, causing "phantom trades" or "phantom closes". Always check `confirmation.value.err` before proceeding.
12. **Execution order matters:** When creating trades via API endpoints, the order MUST be:
1. Open position + place exit orders
2. Save to database (`createTrade()`)
3. Add to Position Manager (`positionManager.addTrade()`)
If Position Manager is added before database save, race conditions occur where monitoring checks before the trade exists in DB.
13. **New trade grace period:** Position Manager skips "external closure" detection for trades <30 seconds old because Drift positions take 5-10 seconds to propagate after opening. Without this grace period, new positions are immediately detected as "closed externally" and cancelled.
14. **Drift minimum position sizes:** Actual minimums differ from documentation:
- SOL-PERP: 0.1 SOL (~$5-15 depending on price)
- ETH-PERP: 0.01 ETH (~$38-40 at $4000/ETH)
- BTC-PERP: 0.0001 BTC (~$10-12 at $100k/BTC)
Always calculate: `minOrderSize × currentPrice` must exceed Drift's $4 minimum. Add buffer for price movement.
15. **Exit reason detection bug:** Position Manager was using current price to determine exit reason, but on-chain orders filled at a DIFFERENT price in the past. Now uses `trade.tp1Hit` / `trade.tp2Hit` flags and realized P&L to correctly identify whether TP1, TP2, or SL triggered. Prevents profitable trades being mislabeled as "SL" exits.
16. **Per-symbol cooldown:** Cooldown period is per-symbol, NOT global. ETH trade at 10:00 does NOT block SOL trade at 10:01. Each coin (SOL/ETH/BTC) has independent cooldown timer to avoid missing opportunities on different assets.
17. **Timeframe-aware scoring crucial:** Signal quality thresholds MUST adjust for 5min vs higher timeframes:
- 5min charts naturally have lower ADX (12-22 healthy) and ATR (0.2-0.7% healthy) than daily charts
- Without timeframe awareness, valid 5min breakouts get blocked as "low quality"
- Anti-chop filter applies -20 points for extreme sideways regardless of timeframe
- Always pass `timeframe` parameter from TradingView alerts to `scoreSignalQuality()`
18. **Price position chasing causes flip-flops:** Opening longs at 90%+ range or shorts at <10% range reliably loses money:
- Database analysis showed overnight flip-flop losses all had price position 9-94% (chasing extremes)
- These trades had valid ADX (16-18) but entered at worst possible time
- Quality scoring now penalizes -15 to -30 points for range extremes
- Prevents rapid reversals when price is already overextended
19. **TradingView ADX minimum for 5min:** Set ADX filter to 15 (not 20+) in TradingView alerts for 5min charts:
- Higher timeframes can use ADX 20+ for strong trends
- 5min charts need lower threshold to catch valid breakouts
- Bot's quality scoring provides second-layer filtering with context-aware metrics
- Two-stage filtering (TradingView + bot) prevents both overtrading and missing valid signals
20. **Prisma Decimal type handling:** Raw SQL queries return Prisma `Decimal` objects, not plain numbers:
- Use `any` type for numeric fields in `$queryRaw` results: `total_pnl: any`
- Convert with `Number()` before returning to frontend: `totalPnL: Number(stat.total_pnl) || 0`
- Frontend uses `.toFixed()` which doesn't exist on Decimal objects
- Applies to all aggregations: SUM(), AVG(), ROUND() - all return Decimal types
- Example: `/api/analytics/version-comparison` converts all numeric fields
21. **JavaScript || vs ?? operators CRITICAL:** When setting default values for numeric config, ALWAYS use `??` (nullish coalescing):
```typescript
// WRONG - treats 0 as falsy:
tp2SizePercent: config.takeProfit2SizePercent || 100 // 0 becomes 100!
// CORRECT - only null/undefined are nullish:
tp2SizePercent: config.takeProfit2SizePercent ?? 100 // 0 stays 0
```
- `||` treats `0`, `false`, `""`, `null`, `undefined` as falsy
- `??` only treats `null` and `undefined` as nullish
- Critical for runner system: `TAKE_PROFIT_2_SIZE_PERCENT=0` must be respected
- This bug caused TP2 orders to be placed at 100% despite config setting 0
- Applies to ALL numeric config values where 0 is valid (TP sizes, leverage, thresholds)
22. **Range-bound chop detection:** The anti-chop filter V2 (implemented 2024-11-10) prevents flip-flop losses:
- Detection: Price position <40% of range + ADX <25 = weak range-bound market
- Penalty: -25 points to signal quality score
- Why: Trades entering early in range with weak trend get whipsawed in seconds
- Evidence: Backtest showed 5 flip-flop trades (8-24 second holds) all had this pattern
- Result: Win rate improved from 43.8% to 55.6%, profit per trade +86%
- Implementation: `lib/trading/signal-quality.ts` checks both conditions before price position scoring
## 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.ts` with env merging
- **Types:** Define interfaces in same file as implementation (not separate types directory)
- **Console logs:** Use emojis for visual scanning: 🎯 🚀 ✅ ❌ 💰 📊 🛡️
## Per-Symbol Trading Controls
**Purpose:** Independent enable/disable toggles and position sizing for SOL and ETH to support different trading strategies (e.g., ETH for data collection at minimal size, SOL for profit generation).
**Configuration Priority:**
1. **Per-symbol ENV vars** (highest priority)
- `SOLANA_ENABLED`, `SOLANA_POSITION_SIZE`, `SOLANA_LEVERAGE`
- `ETHEREUM_ENABLED`, `ETHEREUM_POSITION_SIZE`, `ETHEREUM_LEVERAGE`
2. **Market-specific config** (from `MARKET_CONFIGS` in config/trading.ts)
3. **Global ENV vars** (fallback for BTC and other symbols)
- `MAX_POSITION_SIZE_USD`, `LEVERAGE`
4. **Default config** (lowest priority)
**Settings UI:** `app/settings/page.tsx` has dedicated sections:
- 💎 Solana section: Toggle + position size + leverage + risk calculator
- ⚡ Ethereum section: Toggle + position size + leverage + risk calculator
- 💰 Global fallback: For BTC-PERP and future symbols
**Example usage:**
```typescript
// In execute/test endpoints
const { size, leverage, enabled } = getPositionSizeForSymbol(driftSymbol, config)
if (!enabled) {
return NextResponse.json({
success: false,
error: 'Symbol trading disabled'
}, { status: 400 })
}
```
**Test buttons:** Settings UI has symbol-specific test buttons:
- 💎 Test SOL LONG/SHORT (disabled when `SOLANA_ENABLED=false`)
- ⚡ Test ETH LONG/SHORT (disabled when `ETHEREUM_ENABLED=false`)
## When Making Changes
1. **Adding new config:** Update DEFAULT_TRADING_CONFIG + getConfigFromEnv() + .env file
2. **Adding database fields:** Update prisma/schema.prisma → `npx prisma migrate dev` → `npx prisma generate` → rebuild Docker
3. **Changing order logic:** Test with DRY_RUN=true first, use small position sizes ($10)
4. **API endpoint changes:** Update both endpoint + corresponding n8n workflow JSON (Check Risk and Execute Trade nodes)
5. **Docker changes:** Rebuild with `docker compose build trading-bot` then restart container
6. **Modifying quality score logic:** Update BOTH `/api/trading/check-risk` and `/api/trading/execute` endpoints, ensure timeframe-aware thresholds are synchronized
7. **Exit strategy changes:** Modify Position Manager logic + update on-chain order placement in `placeExitOrders()`
8. **TradingView alert changes:** Ensure alerts pass `timeframe` field (e.g., `"timeframe": "5"`) to enable proper signal quality scoring
## Development Roadmap
See `POSITION_SCALING_ROADMAP.md` for planned optimizations:
- **Phase 1 (✅ COMPLETE):** Collect data with quality scores (20-50 trades needed)
- **Phase 2:** ATR-based dynamic targets (adapt to volatility)
- **Phase 3:** Signal quality-based scaling (high quality = larger runners)
- **Phase 4:** Direction-based optimization (shorts vs longs have different performance)
- **Phase 5 (✅ COMPLETE):** TP2-as-runner system implemented - 25% runner with ATR-based trailing stop
- **Phase 6:** ML-based exit prediction (future)
**Recent Implementation:** TP2-as-runner system provides 5x larger runner (25% vs 5%) for better profit capture on extended moves. When TP2 price is hit, trailing stop activates on full remaining position instead of closing partial amount.
**Data-driven approach:** Each phase requires validation through SQL analysis before implementation. No premature optimization.
**Signal Quality Version Tracking:** Database tracks `signalQualityVersion` field to compare algorithm performance:
- Analytics dashboard shows version comparison: trades, win rate, P&L, extreme position stats
- Focus on extreme positions (< 15% range) - v3 aims to reduce losses from weak ADX entries
- SQL queries in `docs/analysis/SIGNAL_QUALITY_VERSION_ANALYSIS.sql` for deep-dive analysis
- Need 20+ v3 trades before meaningful comparison vs v1/v2 data
## 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.