Complete implementation of adaptive leverage configuration via web interface:
Frontend (app/settings/page.tsx):
- Added 4 fields to TradingSettings interface:
* USE_ADAPTIVE_LEVERAGE: boolean
* HIGH_QUALITY_LEVERAGE: number
* LOW_QUALITY_LEVERAGE: number
* QUALITY_LEVERAGE_THRESHOLD: number
- Added complete Adaptive Leverage section with:
* Purple-themed informational box explaining quality-based leverage
* Toggle switch for enabling/disabling (🎯 Enable Adaptive Leverage)
* Number inputs for high leverage (1-20), low leverage (1-20), threshold (80-100)
* Visual tier display showing leverage multipliers and position sizes
* Dynamic calculation based on $560 free collateral
Backend (app/api/settings/route.ts):
- GET handler: Load 4 adaptive leverage fields from environment variables
- POST handler: Save 4 adaptive leverage fields to .env file
- Proper type conversion (boolean from 'true', numbers from parseInt/parseFloat)
Visual Tier Display Example:
Below Threshold: Blocked (no trade)
Changes enable users to adjust leverage settings via web UI instead of
manually editing .env file and restarting container.
241 lines
11 KiB
TypeScript
241 lines
11 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'),
|
|
MIN_SIGNAL_QUALITY_SCORE_LONG: parseInt(env.MIN_SIGNAL_QUALITY_SCORE_LONG || env.MIN_SIGNAL_QUALITY_SCORE || '60'),
|
|
MIN_SIGNAL_QUALITY_SCORE_SHORT: parseInt(env.MIN_SIGNAL_QUALITY_SCORE_SHORT || env.MIN_SIGNAL_QUALITY_SCORE || '60'),
|
|
SLIPPAGE_TOLERANCE: parseFloat(env.SLIPPAGE_TOLERANCE || '1.0'),
|
|
DRY_RUN: env.DRY_RUN === 'true',
|
|
|
|
// Adaptive Leverage (Dec 1, 2025)
|
|
USE_ADAPTIVE_LEVERAGE: env.USE_ADAPTIVE_LEVERAGE === 'true',
|
|
HIGH_QUALITY_LEVERAGE: parseFloat(env.HIGH_QUALITY_LEVERAGE || '5'),
|
|
LOW_QUALITY_LEVERAGE: parseFloat(env.LOW_QUALITY_LEVERAGE || '1'),
|
|
QUALITY_LEVERAGE_THRESHOLD: parseInt(env.QUALITY_LEVERAGE_THRESHOLD || '95'),
|
|
}
|
|
|
|
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(),
|
|
MIN_SIGNAL_QUALITY_SCORE_LONG: settings.MIN_SIGNAL_QUALITY_SCORE_LONG.toString(),
|
|
MIN_SIGNAL_QUALITY_SCORE_SHORT: settings.MIN_SIGNAL_QUALITY_SCORE_SHORT.toString(),
|
|
SLIPPAGE_TOLERANCE: settings.SLIPPAGE_TOLERANCE.toString(),
|
|
DRY_RUN: settings.DRY_RUN.toString(),
|
|
|
|
// Adaptive Leverage (Dec 1, 2025)
|
|
USE_ADAPTIVE_LEVERAGE: settings.USE_ADAPTIVE_LEVERAGE.toString(),
|
|
HIGH_QUALITY_LEVERAGE: settings.HIGH_QUALITY_LEVERAGE.toString(),
|
|
LOW_QUALITY_LEVERAGE: settings.LOW_QUALITY_LEVERAGE.toString(),
|
|
QUALITY_LEVERAGE_THRESHOLD: settings.QUALITY_LEVERAGE_THRESHOLD.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 }
|
|
)
|
|
}
|
|
}
|
|
|