- Add SymbolSettings interface with enabled/positionSize/leverage fields - Implement per-symbol ENV variables (SOLANA_*, ETHEREUM_*) - Add SOL and ETH sections to settings UI with enable/disable toggles - Add symbol-specific test buttons (SOL LONG/SHORT, ETH LONG/SHORT) - Update execute and test endpoints to check symbol enabled status - Add real-time risk/reward calculator per symbol - Rename 'Position Sizing' to 'Global Fallback' for clarity - Fix position manager P&L calculation for externally closed positions - Fix zero P&L bug affecting 12 historical trades - Add SQL scripts for recalculating historical P&L data - Move archive TypeScript files to .archive to fix build Defaults: - SOL: 10 base × 10x leverage = 100 notional (profit trading) - ETH: base × 1x leverage = notional (data collection) - Global: 10 × 10x for BTC and other symbols Configuration priority: Per-symbol ENV > Market config > Global ENV > Defaults
725 lines
30 KiB
TypeScript
725 lines
30 KiB
TypeScript
/**
|
||
* Trading Bot Settings UI
|
||
*
|
||
* Beautiful interface for managing trading parameters
|
||
*/
|
||
|
||
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
|
||
interface TradingSettings {
|
||
// Global fallback settings
|
||
MAX_POSITION_SIZE_USD: number
|
||
LEVERAGE: number
|
||
|
||
// Per-symbol settings
|
||
SOLANA_ENABLED: boolean
|
||
SOLANA_POSITION_SIZE: number
|
||
SOLANA_LEVERAGE: number
|
||
ETHEREUM_ENABLED: boolean
|
||
ETHEREUM_POSITION_SIZE: number
|
||
ETHEREUM_LEVERAGE: number
|
||
|
||
// Risk management
|
||
STOP_LOSS_PERCENT: number
|
||
TAKE_PROFIT_1_PERCENT: number
|
||
TAKE_PROFIT_1_SIZE_PERCENT: number
|
||
TAKE_PROFIT_2_PERCENT: number
|
||
TAKE_PROFIT_2_SIZE_PERCENT: number
|
||
EMERGENCY_STOP_PERCENT: number
|
||
BREAKEVEN_TRIGGER_PERCENT: number
|
||
PROFIT_LOCK_TRIGGER_PERCENT: number
|
||
PROFIT_LOCK_PERCENT: number
|
||
USE_TRAILING_STOP: boolean
|
||
TRAILING_STOP_PERCENT: number
|
||
TRAILING_STOP_ACTIVATION: number
|
||
MAX_DAILY_DRAWDOWN: number
|
||
MAX_TRADES_PER_HOUR: number
|
||
MIN_TIME_BETWEEN_TRADES: number
|
||
MIN_QUALITY_SCORE: number
|
||
SLIPPAGE_TOLERANCE: number
|
||
DRY_RUN: boolean
|
||
}
|
||
|
||
export default function SettingsPage() {
|
||
const [settings, setSettings] = useState<TradingSettings | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [saving, setSaving] = useState(false)
|
||
const [restarting, setRestarting] = useState(false)
|
||
const [testing, setTesting] = useState(false)
|
||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
|
||
|
||
useEffect(() => {
|
||
loadSettings()
|
||
}, [])
|
||
|
||
const loadSettings = async () => {
|
||
try {
|
||
const response = await fetch('/api/settings')
|
||
const data = await response.json()
|
||
setSettings(data)
|
||
setLoading(false)
|
||
} catch (error) {
|
||
setMessage({ type: 'error', text: 'Failed to load settings' })
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const saveSettings = async () => {
|
||
setSaving(true)
|
||
setMessage(null)
|
||
try {
|
||
const response = await fetch('/api/settings', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(settings),
|
||
})
|
||
|
||
if (response.ok) {
|
||
setMessage({ type: 'success', text: 'Settings saved! Click "Restart Bot" to apply changes.' })
|
||
} else {
|
||
setMessage({ type: 'error', text: 'Failed to save settings' })
|
||
}
|
||
} catch (error) {
|
||
setMessage({ type: 'error', text: 'Failed to save settings' })
|
||
}
|
||
setSaving(false)
|
||
}
|
||
|
||
const restartBot = async () => {
|
||
setRestarting(true)
|
||
setMessage(null)
|
||
try {
|
||
const response = await fetch('/api/restart', {
|
||
method: 'POST',
|
||
})
|
||
|
||
if (response.ok) {
|
||
setMessage({ type: 'success', text: 'Bot is restarting... Settings will be applied in ~10 seconds.' })
|
||
} else {
|
||
setMessage({ type: 'error', text: 'Failed to restart bot. Please restart manually with: docker restart trading-bot' })
|
||
}
|
||
} catch (error) {
|
||
setMessage({ type: 'error', text: 'Failed to restart bot. Please restart manually with: docker restart trading-bot' })
|
||
}
|
||
setRestarting(false)
|
||
}
|
||
|
||
const testTrade = async (direction: 'long' | 'short', symbol: string = 'SOLUSDT') => {
|
||
if (!confirm(`⚠️ This will execute a REAL ${direction.toUpperCase()} trade on ${symbol} with current settings. Continue?`)) {
|
||
return
|
||
}
|
||
|
||
setTesting(true)
|
||
setMessage(null)
|
||
try {
|
||
const response = await fetch('/api/trading/test', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
symbol: symbol,
|
||
direction: direction,
|
||
}),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
const dualStopsMsg = data.useDualStops
|
||
? `Dual stops: Soft $${data.softStopPrice?.toFixed(4)} | Hard $${data.hardStopPrice?.toFixed(4)}`
|
||
: `SL: $${data.stopLoss?.toFixed(4)}`
|
||
setMessage({
|
||
type: 'success',
|
||
text: `✅ ${symbol} ${direction.toUpperCase()} test trade executed! Size: $${data.positionSize?.toFixed(2)} | Entry: $${data.entryPrice?.toFixed(4)} | ${dualStopsMsg} | TX: ${data.positionId?.substring(0, 8)}...`
|
||
})
|
||
} else {
|
||
setMessage({ type: 'error', text: `Failed: ${data.error || data.message}` })
|
||
}
|
||
} catch (error) {
|
||
setMessage({ type: 'error', text: `Test trade failed: ${error instanceof Error ? error.message : 'Unknown error'}` })
|
||
}
|
||
setTesting(false)
|
||
}
|
||
|
||
const updateSetting = (key: keyof TradingSettings, value: any) => {
|
||
if (!settings) return
|
||
setSettings({ ...settings, [key]: value })
|
||
}
|
||
|
||
const calculateRisk = (baseSize?: number, leverage?: number) => {
|
||
if (!settings) return null
|
||
const size = baseSize ?? settings.MAX_POSITION_SIZE_USD
|
||
const lev = leverage ?? settings.LEVERAGE
|
||
const maxLoss = size * lev * (Math.abs(settings.STOP_LOSS_PERCENT) / 100)
|
||
const tp1Gain = size * lev * (settings.TAKE_PROFIT_1_PERCENT / 100) * (settings.TAKE_PROFIT_1_SIZE_PERCENT / 100)
|
||
const tp2Gain = size * lev * (settings.TAKE_PROFIT_2_PERCENT / 100) * (settings.TAKE_PROFIT_2_SIZE_PERCENT / 100)
|
||
const fullWin = tp1Gain + tp2Gain
|
||
|
||
return { maxLoss, tp1Gain, tp2Gain, fullWin }
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center">
|
||
<div className="text-white text-xl">Loading settings...</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!settings) return null
|
||
|
||
const risk = calculateRisk()
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 py-8 px-4">
|
||
<div className="max-w-5xl mx-auto">
|
||
{/* Header */}
|
||
<div className="mb-8">
|
||
<div className="flex items-center space-x-4 mb-4">
|
||
<a href="/" className="text-gray-400 hover:text-white transition">
|
||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||
</svg>
|
||
</a>
|
||
<div>
|
||
<h1 className="text-4xl font-bold text-white">⚙️ Trading Bot Settings</h1>
|
||
</div>
|
||
</div>
|
||
<p className="text-slate-400">Configure your automated trading parameters</p>
|
||
</div>
|
||
|
||
{/* Message */}
|
||
{message && (
|
||
<div className={`mb-6 p-4 rounded-lg ${
|
||
message.type === 'success' ? 'bg-green-500/20 text-green-400 border border-green-500/50' : 'bg-red-500/20 text-red-400 border border-red-500/50'
|
||
}`}>
|
||
{message.text}
|
||
</div>
|
||
)}
|
||
|
||
{/* Risk Calculator */}
|
||
{risk && (
|
||
<div className="mb-8 bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl p-6">
|
||
<h2 className="text-xl font-bold text-white mb-4">📊 Risk Calculator</h2>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="bg-red-500/10 border border-red-500/50 rounded-lg p-4">
|
||
<div className="text-red-400 text-sm mb-1">Max Loss (SL)</div>
|
||
<div className="text-white text-2xl font-bold">-${risk.maxLoss.toFixed(2)}</div>
|
||
</div>
|
||
<div className="bg-blue-500/10 border border-blue-500/50 rounded-lg p-4">
|
||
<div className="text-blue-400 text-sm mb-1">TP1 Gain ({settings.TAKE_PROFIT_1_SIZE_PERCENT}%)</div>
|
||
<div className="text-white text-2xl font-bold">+${risk.tp1Gain.toFixed(2)}</div>
|
||
</div>
|
||
<div className="bg-green-500/10 border border-green-500/50 rounded-lg p-4">
|
||
<div className="text-green-400 text-sm mb-1">TP2 Gain ({settings.TAKE_PROFIT_2_SIZE_PERCENT}%)</div>
|
||
<div className="text-white text-2xl font-bold">+${risk.tp2Gain.toFixed(2)}</div>
|
||
</div>
|
||
<div className="bg-purple-500/10 border border-purple-500/50 rounded-lg p-4">
|
||
<div className="text-purple-400 text-sm mb-1">Full Win</div>
|
||
<div className="text-white text-2xl font-bold">+${risk.fullWin.toFixed(2)}</div>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 text-slate-400 text-sm">
|
||
Risk/Reward Ratio: 1:{(risk.fullWin / risk.maxLoss).toFixed(2)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Settings Sections */}
|
||
<div className="space-y-6">
|
||
{/* Per-Symbol Position Sizing */}
|
||
<Section title="<22> Solana (SOL-PERP)" description="Individual settings for Solana perpetual trading">
|
||
<div className="mb-4 p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||
<p className="text-sm text-purple-400">
|
||
Enable/disable Solana trading and set symbol-specific position sizing. When enabled, these settings override global defaults for SOL trades.
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center justify-between p-4 bg-slate-700/30 rounded-lg mb-4">
|
||
<div className="flex-1">
|
||
<div className="text-white font-medium mb-1">🟢 Enable Solana Trading</div>
|
||
<div className="text-slate-400 text-sm">
|
||
Accept SOL-PERP trade signals from TradingView
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => updateSetting('SOLANA_ENABLED', !settings.SOLANA_ENABLED)}
|
||
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
|
||
settings.SOLANA_ENABLED ? 'bg-green-500' : 'bg-slate-600'
|
||
}`}
|
||
>
|
||
<span
|
||
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
|
||
settings.SOLANA_ENABLED ? 'translate-x-7' : 'translate-x-1'
|
||
}`}
|
||
/>
|
||
</button>
|
||
</div>
|
||
<Setting
|
||
label="SOL Position Size (USD)"
|
||
value={settings.SOLANA_POSITION_SIZE}
|
||
onChange={(v) => updateSetting('SOLANA_POSITION_SIZE', v)}
|
||
min={1}
|
||
max={10000}
|
||
step={1}
|
||
description={`Base capital for SOL trades. With ${settings.SOLANA_LEVERAGE}x leverage = $${(settings.SOLANA_POSITION_SIZE * settings.SOLANA_LEVERAGE).toFixed(0)} notional position.`}
|
||
/>
|
||
<Setting
|
||
label="SOL Leverage"
|
||
value={settings.SOLANA_LEVERAGE}
|
||
onChange={(v) => updateSetting('SOLANA_LEVERAGE', v)}
|
||
min={1}
|
||
max={20}
|
||
step={1}
|
||
description="Leverage multiplier for Solana positions only."
|
||
/>
|
||
{(() => {
|
||
const solRisk = calculateRisk(settings.SOLANA_POSITION_SIZE, settings.SOLANA_LEVERAGE)
|
||
return solRisk && (
|
||
<div className="p-4 bg-slate-700/50 rounded-lg">
|
||
<div className="text-sm text-slate-300 mb-2">SOL Risk/Reward</div>
|
||
<div className="flex gap-4 text-xs">
|
||
<div>
|
||
<span className="text-red-400">Max Loss: </span>
|
||
<span className="text-white font-bold">${solRisk.maxLoss.toFixed(2)}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-green-400">Full Win: </span>
|
||
<span className="text-white font-bold">${solRisk.fullWin.toFixed(2)}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-purple-400">R:R </span>
|
||
<span className="text-white font-bold">1:{(solRisk.fullWin / solRisk.maxLoss).toFixed(2)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
})()}
|
||
</Section>
|
||
|
||
<Section title="⚡ Ethereum (ETH-PERP)" description="Individual settings for Ethereum perpetual trading">
|
||
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||
<p className="text-sm text-blue-400">
|
||
Enable/disable Ethereum trading and set symbol-specific position sizing. When enabled, these settings override global defaults for ETH trades.
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center justify-between p-4 bg-slate-700/30 rounded-lg mb-4">
|
||
<div className="flex-1">
|
||
<div className="text-white font-medium mb-1">🟢 Enable Ethereum Trading</div>
|
||
<div className="text-slate-400 text-sm">
|
||
Accept ETH-PERP trade signals from TradingView
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => updateSetting('ETHEREUM_ENABLED', !settings.ETHEREUM_ENABLED)}
|
||
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
|
||
settings.ETHEREUM_ENABLED ? 'bg-green-500' : 'bg-slate-600'
|
||
}`}
|
||
>
|
||
<span
|
||
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
|
||
settings.ETHEREUM_ENABLED ? 'translate-x-7' : 'translate-x-1'
|
||
}`}
|
||
/>
|
||
</button>
|
||
</div>
|
||
<Setting
|
||
label="ETH Position Size (USD)"
|
||
value={settings.ETHEREUM_POSITION_SIZE}
|
||
onChange={(v) => updateSetting('ETHEREUM_POSITION_SIZE', v)}
|
||
min={1}
|
||
max={10000}
|
||
step={1}
|
||
description={`Base capital for ETH trades. With ${settings.ETHEREUM_LEVERAGE}x leverage = $${(settings.ETHEREUM_POSITION_SIZE * settings.ETHEREUM_LEVERAGE).toFixed(0)} notional position. Drift minimum: ~$38-40 (0.01 ETH).`}
|
||
/>
|
||
<Setting
|
||
label="ETH Leverage"
|
||
value={settings.ETHEREUM_LEVERAGE}
|
||
onChange={(v) => updateSetting('ETHEREUM_LEVERAGE', v)}
|
||
min={1}
|
||
max={20}
|
||
step={1}
|
||
description="Leverage multiplier for Ethereum positions only."
|
||
/>
|
||
{(() => {
|
||
const ethRisk = calculateRisk(settings.ETHEREUM_POSITION_SIZE, settings.ETHEREUM_LEVERAGE)
|
||
return ethRisk && (
|
||
<div className="p-4 bg-slate-700/50 rounded-lg">
|
||
<div className="text-sm text-slate-300 mb-2">ETH Risk/Reward</div>
|
||
<div className="flex gap-4 text-xs">
|
||
<div>
|
||
<span className="text-red-400">Max Loss: </span>
|
||
<span className="text-white font-bold">${ethRisk.maxLoss.toFixed(2)}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-green-400">Full Win: </span>
|
||
<span className="text-white font-bold">${ethRisk.fullWin.toFixed(2)}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-purple-400">R:R </span>
|
||
<span className="text-white font-bold">1:{(ethRisk.fullWin / ethRisk.maxLoss).toFixed(2)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
})()}
|
||
</Section>
|
||
|
||
{/* Global Position Sizing (Fallback) */}
|
||
<Section title="💰 Global Position Sizing (Fallback)" description="Default settings for symbols without specific config (e.g., BTC)">
|
||
<div className="mb-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||
<p className="text-sm text-yellow-400">
|
||
These are fallback values used for any symbol that doesn't have its own specific settings (like BTC-PERP). SOL and ETH ignore these when their own settings are configured above.
|
||
</p>
|
||
</div>
|
||
<Setting
|
||
label="Position Size (USD)"
|
||
value={settings.MAX_POSITION_SIZE_USD}
|
||
onChange={(v) => updateSetting('MAX_POSITION_SIZE_USD', v)}
|
||
min={10}
|
||
max={10000}
|
||
step={10}
|
||
description="Base USD amount per trade for unspecified symbols."
|
||
/>
|
||
<Setting
|
||
label="Leverage"
|
||
value={settings.LEVERAGE}
|
||
onChange={(v) => updateSetting('LEVERAGE', v)}
|
||
min={1}
|
||
max={20}
|
||
step={1}
|
||
description="Leverage multiplier for unspecified symbols."
|
||
/>
|
||
</Section>
|
||
|
||
{/* Risk Management */}
|
||
<Section title="🛡️ Risk Management" description="Stop loss and take profit levels">
|
||
<Setting
|
||
label="Stop Loss (%)"
|
||
value={settings.STOP_LOSS_PERCENT}
|
||
onChange={(v) => updateSetting('STOP_LOSS_PERCENT', v)}
|
||
min={-10}
|
||
max={-0.1}
|
||
step={0.1}
|
||
description="Close 100% of position when price drops this much. Protects from large losses."
|
||
/>
|
||
<Setting
|
||
label="Take Profit 1 Price (%)"
|
||
value={settings.TAKE_PROFIT_1_PERCENT}
|
||
onChange={(v) => updateSetting('TAKE_PROFIT_1_PERCENT', v)}
|
||
min={0.1}
|
||
max={10}
|
||
step={0.1}
|
||
description="Price level for first take profit exit."
|
||
/>
|
||
<Setting
|
||
label="Take Profit 1 Size (%)"
|
||
value={settings.TAKE_PROFIT_1_SIZE_PERCENT}
|
||
onChange={(v) => updateSetting('TAKE_PROFIT_1_SIZE_PERCENT', v)}
|
||
min={1}
|
||
max={100}
|
||
step={1}
|
||
description="What % of position to close at TP1. Example: 50 = close half."
|
||
/>
|
||
<Setting
|
||
label="Take Profit 2 Price (%)"
|
||
value={settings.TAKE_PROFIT_2_PERCENT}
|
||
onChange={(v) => updateSetting('TAKE_PROFIT_2_PERCENT', v)}
|
||
min={0.1}
|
||
max={20}
|
||
step={0.1}
|
||
description="Price level for second take profit exit."
|
||
/>
|
||
<Setting
|
||
label="Take Profit 2 Size (%)"
|
||
value={settings.TAKE_PROFIT_2_SIZE_PERCENT}
|
||
onChange={(v) => updateSetting('TAKE_PROFIT_2_SIZE_PERCENT', v)}
|
||
min={1}
|
||
max={100}
|
||
step={1}
|
||
description="What % of remaining position to close at TP2. Example: 100 = close rest."
|
||
/>
|
||
<Setting
|
||
label="Emergency Stop (%)"
|
||
value={settings.EMERGENCY_STOP_PERCENT}
|
||
onChange={(v) => updateSetting('EMERGENCY_STOP_PERCENT', v)}
|
||
min={-20}
|
||
max={-0.1}
|
||
step={0.1}
|
||
description="Hard stop for flash crashes. Should be wider than regular SL."
|
||
/>
|
||
</Section>
|
||
|
||
{/* Dynamic Adjustments */}
|
||
<Section title="🎯 Dynamic Stop Loss" description="Automatically adjust SL as trade moves in profit">
|
||
<Setting
|
||
label="Breakeven Trigger (%)"
|
||
value={settings.BREAKEVEN_TRIGGER_PERCENT}
|
||
onChange={(v) => updateSetting('BREAKEVEN_TRIGGER_PERCENT', v)}
|
||
min={0}
|
||
max={5}
|
||
step={0.1}
|
||
description="After TP1 closes, move SL to this profit level. Should be between 0% (breakeven) and TP1%. Example: 0.4% = locks in +4% account profit on remaining position."
|
||
/>
|
||
<Setting
|
||
label="Profit Lock Trigger (%)"
|
||
value={settings.PROFIT_LOCK_TRIGGER_PERCENT}
|
||
onChange={(v) => updateSetting('PROFIT_LOCK_TRIGGER_PERCENT', v)}
|
||
min={0}
|
||
max={10}
|
||
step={0.1}
|
||
description="After TP1, if price continues to this level, move SL higher to lock more profit. Must be > TP1 and < TP2."
|
||
/>
|
||
<Setting
|
||
label="Profit Lock Amount (%)"
|
||
value={settings.PROFIT_LOCK_PERCENT}
|
||
onChange={(v) => updateSetting('PROFIT_LOCK_PERCENT', v)}
|
||
min={0}
|
||
max={5}
|
||
step={0.1}
|
||
description="When Profit Lock Trigger hits, move SL to this profit level. Should be > Breakeven Trigger."
|
||
/>
|
||
</Section>
|
||
|
||
{/* Trailing Stop */}
|
||
<Section title="🏃 Trailing Stop (Runner)" description="Let a small portion run with dynamic stop loss">
|
||
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||
<p className="text-sm text-blue-400">
|
||
After TP2 closes, the remaining position (your "runner") can use a trailing stop loss that follows price.
|
||
This lets you capture big moves while protecting profit.
|
||
</p>
|
||
</div>
|
||
<Setting
|
||
label="Use Trailing Stop"
|
||
value={settings.USE_TRAILING_STOP ? 1 : 0}
|
||
onChange={(v) => updateSetting('USE_TRAILING_STOP', v === 1)}
|
||
min={0}
|
||
max={1}
|
||
step={1}
|
||
description="Enable trailing stop for runner position after TP2. 0 = disabled, 1 = enabled."
|
||
/>
|
||
<Setting
|
||
label="Trailing Stop Distance (%)"
|
||
value={settings.TRAILING_STOP_PERCENT}
|
||
onChange={(v) => updateSetting('TRAILING_STOP_PERCENT', v)}
|
||
min={0.1}
|
||
max={2}
|
||
step={0.1}
|
||
description="How far below peak price (for longs) to trail the stop loss. Example: 0.3% = SL trails 0.3% below highest price reached."
|
||
/>
|
||
<Setting
|
||
label="Trailing Stop Activation (%)"
|
||
value={settings.TRAILING_STOP_ACTIVATION}
|
||
onChange={(v) => updateSetting('TRAILING_STOP_ACTIVATION', v)}
|
||
min={0.1}
|
||
max={5}
|
||
step={0.1}
|
||
description="Runner must reach this profit % before trailing stop activates. Prevents premature stops. Example: 0.5% = wait until runner is +0.5% profit."
|
||
/>
|
||
</Section>
|
||
|
||
{/* Trade Limits */}
|
||
<Section title="⚠️ Safety Limits" description="Prevent overtrading and excessive losses">
|
||
<Setting
|
||
label="Max Daily Loss (USD)"
|
||
value={settings.MAX_DAILY_DRAWDOWN}
|
||
onChange={(v) => updateSetting('MAX_DAILY_DRAWDOWN', v)}
|
||
min={-1000}
|
||
max={-10}
|
||
step={10}
|
||
description="Stop trading if daily loss exceeds this amount."
|
||
/>
|
||
<Setting
|
||
label="Max Trades Per Hour"
|
||
value={settings.MAX_TRADES_PER_HOUR}
|
||
onChange={(v) => updateSetting('MAX_TRADES_PER_HOUR', v)}
|
||
min={1}
|
||
max={20}
|
||
step={1}
|
||
description="Maximum number of trades allowed per hour."
|
||
/>
|
||
<Setting
|
||
label="Cooldown Between Trades (minutes)"
|
||
value={settings.MIN_TIME_BETWEEN_TRADES}
|
||
onChange={(v) => updateSetting('MIN_TIME_BETWEEN_TRADES', v)}
|
||
min={0}
|
||
max={60}
|
||
step={1}
|
||
description="Minimum wait time between trades to prevent overtrading."
|
||
/>
|
||
<Setting
|
||
label="Min Quality Score (0-100)"
|
||
value={settings.MIN_QUALITY_SCORE}
|
||
onChange={(v) => updateSetting('MIN_QUALITY_SCORE', v)}
|
||
min={0}
|
||
max={100}
|
||
step={5}
|
||
description="Minimum signal quality score required to accept a trade. Signals below this score will be blocked."
|
||
/>
|
||
</Section>
|
||
|
||
{/* Execution */}
|
||
<Section title="⚡ Execution Settings" description="Order execution parameters">
|
||
<Setting
|
||
label="Slippage Tolerance (%)"
|
||
value={settings.SLIPPAGE_TOLERANCE}
|
||
onChange={(v) => updateSetting('SLIPPAGE_TOLERANCE', v)}
|
||
min={0.1}
|
||
max={5}
|
||
step={0.1}
|
||
description="Maximum acceptable price slippage on market orders."
|
||
/>
|
||
<div className="flex items-center justify-between p-4 bg-slate-700/30 rounded-lg">
|
||
<div className="flex-1">
|
||
<div className="text-white font-medium mb-1">🧪 Dry Run Mode</div>
|
||
<div className="text-slate-400 text-sm">
|
||
Simulate trades without executing. Enable for testing.
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => updateSetting('DRY_RUN', !settings.DRY_RUN)}
|
||
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
|
||
settings.DRY_RUN ? 'bg-blue-500' : 'bg-slate-600'
|
||
}`}
|
||
>
|
||
<span
|
||
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
|
||
settings.DRY_RUN ? 'translate-x-7' : 'translate-x-1'
|
||
}`}
|
||
/>
|
||
</button>
|
||
</div>
|
||
</Section>
|
||
</div>
|
||
|
||
{/* Action Buttons */}
|
||
<div className="mt-8 space-y-4">
|
||
{/* Primary Actions */}
|
||
<div className="flex gap-4">
|
||
<button
|
||
onClick={saveSettings}
|
||
disabled={saving}
|
||
className="flex-1 bg-gradient-to-r from-blue-500 to-purple-500 text-white font-bold py-4 px-6 rounded-lg hover:from-blue-600 hover:to-purple-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{saving ? '💾 Saving...' : '💾 Save Settings'}
|
||
</button>
|
||
<button
|
||
onClick={restartBot}
|
||
disabled={restarting}
|
||
className="flex-1 bg-gradient-to-r from-green-500 to-emerald-500 text-white font-bold py-4 px-6 rounded-lg hover:from-green-600 hover:to-emerald-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{restarting ? '🔄 Restarting...' : '🔄 Restart Bot'}
|
||
</button>
|
||
<button
|
||
onClick={loadSettings}
|
||
className="bg-slate-700 text-white font-bold py-4 px-6 rounded-lg hover:bg-slate-600 transition-all"
|
||
>
|
||
↺ Reset
|
||
</button>
|
||
</div>
|
||
|
||
{/* Test Trade Buttons */}
|
||
<div className="space-y-4">
|
||
<div className="text-center text-slate-300 text-sm font-bold">🧪 Test Trades (REAL MONEY)</div>
|
||
<div className="flex gap-4">
|
||
<button
|
||
onClick={() => testTrade('long', 'SOLUSDT')}
|
||
disabled={testing || !settings.SOLANA_ENABLED}
|
||
className="flex-1 bg-gradient-to-r from-purple-500 to-purple-600 text-white font-bold py-4 px-6 rounded-lg hover:from-purple-600 hover:to-purple-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-purple-400"
|
||
>
|
||
{testing ? '🧪 Executing...' : '💎 Test SOL LONG'}
|
||
</button>
|
||
<button
|
||
onClick={() => testTrade('short', 'SOLUSDT')}
|
||
disabled={testing || !settings.SOLANA_ENABLED}
|
||
className="flex-1 bg-gradient-to-r from-purple-600 to-red-500 text-white font-bold py-4 px-6 rounded-lg hover:from-purple-700 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-purple-400"
|
||
>
|
||
{testing ? '🧪 Executing...' : '💎 Test SOL SHORT'}
|
||
</button>
|
||
</div>
|
||
<div className="flex gap-4">
|
||
<button
|
||
onClick={() => testTrade('long', 'ETHUSDT')}
|
||
disabled={testing || !settings.ETHEREUM_ENABLED}
|
||
className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white font-bold py-4 px-6 rounded-lg hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-blue-400"
|
||
>
|
||
{testing ? '🧪 Executing...' : '⚡ Test ETH LONG'}
|
||
</button>
|
||
<button
|
||
onClick={() => testTrade('short', 'ETHUSDT')}
|
||
disabled={testing || !settings.ETHEREUM_ENABLED}
|
||
className="flex-1 bg-gradient-to-r from-blue-600 to-red-500 text-white font-bold py-4 px-6 rounded-lg hover:from-blue-700 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-blue-400"
|
||
>
|
||
{testing ? '🧪 Executing...' : '⚡ Test ETH SHORT'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 text-center text-slate-400 text-sm">
|
||
💡 Save settings first, then click Restart Bot to apply changes
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Section({ title, description, children }: { title: string, description: string, children: React.ReactNode }) {
|
||
return (
|
||
<div className="bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-xl p-6">
|
||
<h2 className="text-xl font-bold text-white mb-1">{title}</h2>
|
||
<p className="text-slate-400 text-sm mb-6">{description}</p>
|
||
<div className="space-y-4">
|
||
{children}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Setting({
|
||
label,
|
||
value,
|
||
onChange,
|
||
min,
|
||
max,
|
||
step,
|
||
description
|
||
}: {
|
||
label: string
|
||
value: number
|
||
onChange: (value: number) => void
|
||
min: number
|
||
max: number
|
||
step: number
|
||
description: string
|
||
}) {
|
||
return (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<label className="text-white font-medium">{label}</label>
|
||
<input
|
||
type="number"
|
||
value={value}
|
||
onChange={(e) => onChange(parseFloat(e.target.value))}
|
||
min={min}
|
||
max={max}
|
||
step={step}
|
||
className="w-24 bg-slate-700 text-white px-3 py-2 rounded-lg border border-slate-600 focus:border-blue-500 focus:outline-none"
|
||
/>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
value={value}
|
||
onChange={(e) => onChange(parseFloat(e.target.value))}
|
||
min={min}
|
||
max={max}
|
||
step={step}
|
||
className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer slider"
|
||
/>
|
||
<p className="text-slate-400 text-sm">{description}</p>
|
||
</div>
|
||
)
|
||
}
|