Files
trading_bot_v4/app/api/settings/route.ts
mindesbunister a7c593077d critical: Fix duplicate Telegram notifications + settings UI restart requirement
Issue #1: Duplicate Telegram Notifications (Nov 23, 2025)
Symptom: Manual closures sent 2x identical notifications
Root Cause: Monitoring loop processes trades from array snapshot, trade removed
during async processing but loop continues with stale reference

Real Incident:
- Trade cmibdii4k0004pe07nzfmturo (SHORT SOL)
- Entry $128.85, Exit $128.79, P&L +$6.44
- Duplicate 'POSITION CLOSED' messages sent
- Logs show 'Manual closure recorded' twice
- Database saved correctly (only once)

Fix (lib/trading/position-manager.ts):
Added guard at start of checkTradeConditions():
```typescript
  console.log(`⏭️ Skipping ${trade.symbol} - already removed`)
  return
}
```

Why needed: handlePriceUpdate() collects trades into array BEFORE async processing
Loop continues even after handleManualClosure() removes trade from Map
Second iteration processes removed trade → duplicate notification

Issue #2: Settings UI Changes Require Container Restart (Nov 23, 2025)
Symptom: Quality threshold raised to 91 via settings UI, but trade with quality 90
still executed (should've been blocked)

Timeline:
- Nov 21 18:55: Threshold raised to 91 in code (commit 08482b4)
- Nov 22 15:08: Container restarted
- Nov 22 16:15: Trade #9 quality 90 executed  (should've blocked)
- .env file had MIN_SIGNAL_QUALITY_SCORE=81 (old value)

Root Cause: Settings API writes to .env but in-memory process.env update doesn't
propagate to all modules. Container restart required for full effect.

Fix (app/api/settings/route.ts):
Added console warning: "⚠️ Container restart recommended"
Changed comment from "immediate effect" to "temporary, may not persist"

User Impact:
- Settings changes via UI now show proper expectations
- Manual .env edit + restart remains required for critical settings
- Future: Add /api/restart call after settings save

Trade #9 Analysis (quality 90, should've been blocked):
- ADX: 17.8 (weak, below 18 minimum)
- Price Position: 98.6% (extreme high, chasing top)
- Loss: -$22.41 (-0.15%)
- Result: Validates quality 91 threshold works correctly

Commits: 08482b4 (threshold raise), this commit (duplicate fix + restart requirement)
2025-11-23 10:57:32 +01:00

225 lines
9.6 KiB
TypeScript

/**
* Settings API Endpoint
*
* Read and update trading bot configuration
*/
import { NextRequest, NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import { DEFAULT_TRADING_CONFIG } from '@/config/trading'
const ENV_FILE_PATH = path.join(process.cwd(), '.env')
function parseEnvFile(): Record<string, string> {
try {
const content = fs.readFileSync(ENV_FILE_PATH, 'utf-8')
const env: Record<string, string> = {}
content.split('\n').forEach(line => {
// Skip comments and empty lines
if (line.trim().startsWith('#') || !line.trim()) return
const match = line.match(/^([A-Z0-9_]+)=(.*)$/)
if (match) {
env[match[1]] = match[2]
}
})
return env
} catch (error) {
console.error('Failed to read .env file:', error)
return {}
}
}
function updateEnvFile(updates: Record<string, any>) {
try {
let content = fs.readFileSync(ENV_FILE_PATH, 'utf-8')
// Update each setting
Object.entries(updates).forEach(([key, value]) => {
const regex = new RegExp(`^${key}=.*$`, 'm')
const newLine = `${key}=${value}`
if (regex.test(content)) {
content = content.replace(regex, newLine)
} else {
// Add new line if key doesn't exist
content += `\n${newLine}`
}
})
fs.writeFileSync(ENV_FILE_PATH, content, 'utf-8')
// Note: Changes to .env require container restart to take full effect
// In-memory updates below are temporary and may not persist across all modules
Object.entries(updates).forEach(([key, value]) => {
process.env[key] = value.toString()
})
console.log('⚠️ Settings updated in .env file. Container restart recommended for all changes to take effect.')
return true
} catch (error) {
console.error('Failed to write .env file:', error)
return false
}
}
export async function GET() {
try {
const env = parseEnvFile()
const settings = {
// Global fallback
MAX_POSITION_SIZE_USD: parseFloat(env.MAX_POSITION_SIZE_USD || '50'),
LEVERAGE: parseFloat(env.LEVERAGE || '5'),
USE_PERCENTAGE_SIZE: env.USE_PERCENTAGE_SIZE === 'true',
// Per-symbol settings
SOLANA_ENABLED: env.SOLANA_ENABLED !== 'false',
SOLANA_POSITION_SIZE: parseFloat(env.SOLANA_POSITION_SIZE || '210'),
SOLANA_LEVERAGE: parseFloat(env.SOLANA_LEVERAGE || '10'),
SOLANA_USE_PERCENTAGE_SIZE: env.SOLANA_USE_PERCENTAGE_SIZE === 'true',
ETHEREUM_ENABLED: env.ETHEREUM_ENABLED !== 'false',
ETHEREUM_POSITION_SIZE: parseFloat(env.ETHEREUM_POSITION_SIZE || '4'),
ETHEREUM_LEVERAGE: parseFloat(env.ETHEREUM_LEVERAGE || '1'),
ETHEREUM_USE_PERCENTAGE_SIZE: env.ETHEREUM_USE_PERCENTAGE_SIZE === 'true',
// Risk management
STOP_LOSS_PERCENT: parseFloat(env.STOP_LOSS_PERCENT || '-1.5'),
TAKE_PROFIT_1_PERCENT: parseFloat(env.TAKE_PROFIT_1_PERCENT || '0.7'),
TAKE_PROFIT_1_SIZE_PERCENT: parseFloat(env.TAKE_PROFIT_1_SIZE_PERCENT || '50'),
TAKE_PROFIT_2_PERCENT: parseFloat(env.TAKE_PROFIT_2_PERCENT || '1.5'),
TAKE_PROFIT_2_SIZE_PERCENT: parseFloat(env.TAKE_PROFIT_2_SIZE_PERCENT || '50'),
EMERGENCY_STOP_PERCENT: parseFloat(env.EMERGENCY_STOP_PERCENT || '-2.0'),
BREAKEVEN_TRIGGER_PERCENT: parseFloat(env.BREAKEVEN_TRIGGER_PERCENT || '0.4'),
PROFIT_LOCK_TRIGGER_PERCENT: parseFloat(env.PROFIT_LOCK_TRIGGER_PERCENT || '1.0'),
PROFIT_LOCK_PERCENT: parseFloat(env.PROFIT_LOCK_PERCENT || '0.4'),
USE_TRAILING_STOP: env.USE_TRAILING_STOP === 'true' || env.USE_TRAILING_STOP === undefined,
TRAILING_STOP_PERCENT: parseFloat(env.TRAILING_STOP_PERCENT || '0.3'),
TRAILING_STOP_ATR_MULTIPLIER: parseFloat(env.TRAILING_STOP_ATR_MULTIPLIER || '1.5'),
TRAILING_STOP_MIN_PERCENT: parseFloat(env.TRAILING_STOP_MIN_PERCENT || '0.25'),
TRAILING_STOP_MAX_PERCENT: parseFloat(env.TRAILING_STOP_MAX_PERCENT || '0.9'),
TRAILING_STOP_ACTIVATION: parseFloat(env.TRAILING_STOP_ACTIVATION || '0.5'),
// ATR-based Dynamic Targets
USE_ATR_BASED_TARGETS: env.USE_ATR_BASED_TARGETS === 'true' || env.USE_ATR_BASED_TARGETS === undefined,
ATR_MULTIPLIER_FOR_TP2: parseFloat(env.ATR_MULTIPLIER_FOR_TP2 || '2.0'),
MIN_TP2_PERCENT: parseFloat(env.MIN_TP2_PERCENT || '0.7'),
MAX_TP2_PERCENT: parseFloat(env.MAX_TP2_PERCENT || '3.0'),
// Position Scaling
ENABLE_POSITION_SCALING: env.ENABLE_POSITION_SCALING === 'true',
MIN_SCALE_QUALITY_SCORE: parseInt(env.MIN_SCALE_QUALITY_SCORE || '75'),
MIN_PROFIT_FOR_SCALE: parseFloat(env.MIN_PROFIT_FOR_SCALE || '0.4'),
MAX_SCALE_MULTIPLIER: parseFloat(env.MAX_SCALE_MULTIPLIER || '2.0'),
SCALE_SIZE_PERCENT: parseFloat(env.SCALE_SIZE_PERCENT || '50'),
MIN_ADX_INCREASE: parseFloat(env.MIN_ADX_INCREASE || '5'),
MAX_PRICE_POSITION_FOR_SCALE: parseFloat(env.MAX_PRICE_POSITION_FOR_SCALE || '70'),
// Safety
MAX_DAILY_DRAWDOWN: parseFloat(env.MAX_DAILY_DRAWDOWN || '-50'),
MAX_TRADES_PER_HOUR: parseInt(env.MAX_TRADES_PER_HOUR || '6'),
MIN_TIME_BETWEEN_TRADES: parseInt(env.MIN_TIME_BETWEEN_TRADES || '600'),
MIN_SIGNAL_QUALITY_SCORE: parseInt(env.MIN_SIGNAL_QUALITY_SCORE || '60'),
SLIPPAGE_TOLERANCE: parseFloat(env.SLIPPAGE_TOLERANCE || '1.0'),
DRY_RUN: env.DRY_RUN === 'true',
}
return NextResponse.json(settings)
} catch (error) {
console.error('Failed to load settings:', error)
return NextResponse.json(
{ error: 'Failed to load settings' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const settings = await request.json()
const updates = {
MAX_POSITION_SIZE_USD: settings.MAX_POSITION_SIZE_USD.toString(),
LEVERAGE: settings.LEVERAGE.toString(),
// Per-symbol settings
SOLANA_ENABLED: settings.SOLANA_ENABLED.toString(),
SOLANA_POSITION_SIZE: settings.SOLANA_POSITION_SIZE.toString(),
SOLANA_LEVERAGE: settings.SOLANA_LEVERAGE.toString(),
ETHEREUM_ENABLED: settings.ETHEREUM_ENABLED.toString(),
ETHEREUM_POSITION_SIZE: settings.ETHEREUM_POSITION_SIZE.toString(),
ETHEREUM_LEVERAGE: settings.ETHEREUM_LEVERAGE.toString(),
// Risk management
STOP_LOSS_PERCENT: settings.STOP_LOSS_PERCENT.toString(),
TAKE_PROFIT_1_PERCENT: settings.TAKE_PROFIT_1_PERCENT.toString(),
TAKE_PROFIT_1_SIZE_PERCENT: settings.TAKE_PROFIT_1_SIZE_PERCENT.toString(),
TAKE_PROFIT_2_PERCENT: settings.TAKE_PROFIT_2_PERCENT.toString(),
TAKE_PROFIT_2_SIZE_PERCENT: settings.TAKE_PROFIT_2_SIZE_PERCENT.toString(),
EMERGENCY_STOP_PERCENT: settings.EMERGENCY_STOP_PERCENT.toString(),
BREAKEVEN_TRIGGER_PERCENT: settings.BREAKEVEN_TRIGGER_PERCENT.toString(),
PROFIT_LOCK_TRIGGER_PERCENT: settings.PROFIT_LOCK_TRIGGER_PERCENT.toString(),
PROFIT_LOCK_PERCENT: settings.PROFIT_LOCK_PERCENT.toString(),
USE_TRAILING_STOP: settings.USE_TRAILING_STOP.toString(),
TRAILING_STOP_PERCENT: settings.TRAILING_STOP_PERCENT.toString(),
TRAILING_STOP_ATR_MULTIPLIER: (settings.TRAILING_STOP_ATR_MULTIPLIER ?? DEFAULT_TRADING_CONFIG.trailingStopAtrMultiplier).toString(),
TRAILING_STOP_MIN_PERCENT: (settings.TRAILING_STOP_MIN_PERCENT ?? DEFAULT_TRADING_CONFIG.trailingStopMinPercent).toString(),
TRAILING_STOP_MAX_PERCENT: (settings.TRAILING_STOP_MAX_PERCENT ?? DEFAULT_TRADING_CONFIG.trailingStopMaxPercent).toString(),
TRAILING_STOP_ACTIVATION: settings.TRAILING_STOP_ACTIVATION.toString(),
// ATR-based Dynamic Targets
USE_ATR_BASED_TARGETS: (settings as any).USE_ATR_BASED_TARGETS?.toString() || 'true',
ATR_MULTIPLIER_FOR_TP2: (settings as any).ATR_MULTIPLIER_FOR_TP2?.toString() || '2.0',
MIN_TP2_PERCENT: (settings as any).MIN_TP2_PERCENT?.toString() || '0.7',
MAX_TP2_PERCENT: (settings as any).MAX_TP2_PERCENT?.toString() || '3.0',
// Position Scaling
ENABLE_POSITION_SCALING: settings.ENABLE_POSITION_SCALING.toString(),
MIN_SCALE_QUALITY_SCORE: settings.MIN_SCALE_QUALITY_SCORE.toString(),
MIN_PROFIT_FOR_SCALE: settings.MIN_PROFIT_FOR_SCALE.toString(),
MAX_SCALE_MULTIPLIER: settings.MAX_SCALE_MULTIPLIER.toString(),
SCALE_SIZE_PERCENT: settings.SCALE_SIZE_PERCENT.toString(),
MIN_ADX_INCREASE: settings.MIN_ADX_INCREASE.toString(),
MAX_PRICE_POSITION_FOR_SCALE: settings.MAX_PRICE_POSITION_FOR_SCALE.toString(),
// Safety
MAX_DAILY_DRAWDOWN: settings.MAX_DAILY_DRAWDOWN.toString(),
MAX_TRADES_PER_HOUR: settings.MAX_TRADES_PER_HOUR.toString(),
MIN_TIME_BETWEEN_TRADES: settings.MIN_TIME_BETWEEN_TRADES.toString(),
MIN_SIGNAL_QUALITY_SCORE: settings.MIN_SIGNAL_QUALITY_SCORE.toString(),
SLIPPAGE_TOLERANCE: settings.SLIPPAGE_TOLERANCE.toString(),
DRY_RUN: settings.DRY_RUN.toString(),
}
const success = updateEnvFile(updates)
if (success) {
try {
const { getPositionManager } = await import('@/lib/trading/position-manager')
const manager = getPositionManager()
manager.refreshConfig()
console.log('⚙️ Position manager config refreshed after settings update')
} catch (pmError) {
console.error('Failed to refresh position manager config:', pmError)
}
return NextResponse.json({ success: true })
} else {
return NextResponse.json(
{ error: 'Failed to save settings' },
{ status: 500 }
)
}
} catch (error) {
console.error('Failed to save settings:', error)
return NextResponse.json(
{ error: 'Failed to save settings' },
{ status: 500 }
)
}
}