- Added restart button to settings page - Created /api/restart endpoint (file-flag based) - Implemented watch-restart.sh daemon - Added systemd service for restart watcher - Updated README with restart setup instructions - Container automatically restarts when settings changed Settings flow: 1. User edits settings in web UI 2. Click 'Save Settings' to persist to .env 3. Click 'Restart Bot' to apply changes 4. Watcher detects flag and restarts container 5. New settings loaded automatically
412 lines
15 KiB
TypeScript
412 lines
15 KiB
TypeScript
/**
|
||
* Trading Bot Settings UI
|
||
*
|
||
* Beautiful interface for managing trading parameters
|
||
*/
|
||
|
||
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
|
||
interface TradingSettings {
|
||
MAX_POSITION_SIZE_USD: number
|
||
LEVERAGE: number
|
||
STOP_LOSS_PERCENT: number
|
||
TAKE_PROFIT_1_PERCENT: number
|
||
TAKE_PROFIT_2_PERCENT: number
|
||
EMERGENCY_STOP_PERCENT: number
|
||
BREAKEVEN_TRIGGER_PERCENT: number
|
||
PROFIT_LOCK_TRIGGER_PERCENT: number
|
||
PROFIT_LOCK_PERCENT: number
|
||
MAX_DAILY_DRAWDOWN: number
|
||
MAX_TRADES_PER_HOUR: number
|
||
MIN_TIME_BETWEEN_TRADES: 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 [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 updateSetting = (key: keyof TradingSettings, value: any) => {
|
||
if (!settings) return
|
||
setSettings({ ...settings, [key]: value })
|
||
}
|
||
|
||
const calculateRisk = () => {
|
||
if (!settings) return null
|
||
const maxLoss = settings.MAX_POSITION_SIZE_USD * settings.LEVERAGE * (Math.abs(settings.STOP_LOSS_PERCENT) / 100)
|
||
const tp1Gain = settings.MAX_POSITION_SIZE_USD * settings.LEVERAGE * (settings.TAKE_PROFIT_1_PERCENT / 100)
|
||
const tp2Gain = settings.MAX_POSITION_SIZE_USD * settings.LEVERAGE * (settings.TAKE_PROFIT_2_PERCENT / 100)
|
||
const fullWin = tp1Gain / 2 + tp2Gain / 2 // 50% at each TP
|
||
|
||
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">
|
||
<h1 className="text-4xl font-bold text-white mb-2">⚙️ Trading Bot Settings</h1>
|
||
<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 (50%)</div>
|
||
<div className="text-white text-2xl font-bold">+${(risk.tp1Gain / 2).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 (50%)</div>
|
||
<div className="text-white text-2xl font-bold">+${(risk.tp2Gain / 2).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">
|
||
{/* Position Sizing */}
|
||
<Section title="💰 Position Sizing" description="Control your trade size and leverage">
|
||
<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. With 5x leverage, $50 = $250 position."
|
||
/>
|
||
<Setting
|
||
label="Leverage"
|
||
value={settings.LEVERAGE}
|
||
onChange={(v) => updateSetting('LEVERAGE', v)}
|
||
min={1}
|
||
max={20}
|
||
step={1}
|
||
description="Multiplier for your position. Higher = more profit AND more risk."
|
||
/>
|
||
</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 (%)"
|
||
value={settings.TAKE_PROFIT_1_PERCENT}
|
||
onChange={(v) => updateSetting('TAKE_PROFIT_1_PERCENT', v)}
|
||
min={0.1}
|
||
max={10}
|
||
step={0.1}
|
||
description="Close 50% of position at this profit level. Locks in early gains."
|
||
/>
|
||
<Setting
|
||
label="Take Profit 2 (%)"
|
||
value={settings.TAKE_PROFIT_2_PERCENT}
|
||
onChange={(v) => updateSetting('TAKE_PROFIT_2_PERCENT', v)}
|
||
min={0.1}
|
||
max={20}
|
||
step={0.1}
|
||
description="Close remaining 50% at this profit level. Captures larger moves."
|
||
/>
|
||
<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="Move SL to breakeven (entry price) when profit reaches this level."
|
||
/>
|
||
<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="When profit reaches this level, lock in profit by moving SL."
|
||
/>
|
||
<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="Move SL to this profit level when lock trigger is hit."
|
||
/>
|
||
</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 (seconds)"
|
||
value={settings.MIN_TIME_BETWEEN_TRADES}
|
||
onChange={(v) => updateSetting('MIN_TIME_BETWEEN_TRADES', v)}
|
||
min={0}
|
||
max={3600}
|
||
step={60}
|
||
description="Minimum wait time between trades to prevent overtrading."
|
||
/>
|
||
</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 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>
|
||
|
||
<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>
|
||
)
|
||
}
|