514 lines
30 KiB
TypeScript
514 lines
30 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
ShieldCheck,
|
|
Scale,
|
|
Zap,
|
|
DollarSign,
|
|
Clock,
|
|
ChevronRight,
|
|
ChevronLeft,
|
|
CheckCircle2,
|
|
AlertTriangle,
|
|
Info,
|
|
Wallet,
|
|
Target,
|
|
Lock
|
|
} from 'lucide-react';
|
|
import { RISK_STYLE_TEMPLATES } from '../lib/RiskStyleTemplates';
|
|
import type { RiskStyleTemplate } from '../lib/RiskStyleTemplates';
|
|
import { getUserTier, TIER_POLICIES, isFeatureAllowed } from '../lib/TierPolicy';
|
|
import { useAuth } from './AuthContext';
|
|
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
|
import { useBacktestFeatureGate } from '../backtest/useBacktestFeatureGate';
|
|
import { createTradeProfile, updateTradeProfile } from '../lib/profileApi';
|
|
|
|
interface WizardState {
|
|
step: number;
|
|
riskStyle: RiskStyleTemplate | null;
|
|
assets: string[];
|
|
capital: number;
|
|
profitTarget: number;
|
|
hours: '24/7' | 'London + New York' | 'Asia only';
|
|
}
|
|
|
|
const ASSETS = [
|
|
{ id: 'BTC/USDT', label: 'Bitcoin (BTC)' },
|
|
{ id: 'ETH/USDT', label: 'Ethereum (ETH)' },
|
|
{ id: 'SOL/USDT', label: 'Solana (SOL)' },
|
|
];
|
|
|
|
const SESSION_MAP = {
|
|
'24/7': 'LDN,NY,TOK,SYD',
|
|
'London + New York': 'LDN,NY',
|
|
'Asia only': 'TOK,SYD'
|
|
};
|
|
|
|
const buildStrategyConfig = (state: WizardState) => ({
|
|
rules: [
|
|
{ ruleId: 'RiskManagementRule', enabled: true, ruleType: 'mandatory', params: {} },
|
|
{ ruleId: 'SessionRule', enabled: state.hours !== '24/7', ruleType: 'mandatory', params: { sessions: SESSION_MAP[state.hours] } },
|
|
...((state.riskStyle?.mandatoryRules || [])
|
|
.filter((ruleId) => !['RiskManagementRule', 'SessionRule'].includes(ruleId))
|
|
.map((ruleId) => ({ ruleId, enabled: true, ruleType: 'mandatory', params: {} }))),
|
|
...((state.riskStyle?.votingRules || []).map((ruleId) => ({ ruleId, enabled: true, ruleType: 'voting', params: {} })))
|
|
],
|
|
riskLimits: {
|
|
maxDailyLossUsd: Math.floor(state.capital * 0.05),
|
|
dailyProfitTargetUsd: state.profitTarget,
|
|
maxOpenTrades: 3,
|
|
maxConsecutiveLosses: 3
|
|
},
|
|
execution: {
|
|
orderType: 'market',
|
|
cooldownMinutes: 60,
|
|
minRulePassRatio: state.riskStyle?.minRulePassRatio || 1,
|
|
entryMode: 'both'
|
|
}
|
|
});
|
|
|
|
export const StrategyWizard: React.FC<{
|
|
onComplete: () => void;
|
|
editingProfile?: any;
|
|
profile?: any;
|
|
previewAsCustomer?: boolean;
|
|
}> = ({ onComplete, editingProfile, profile: userProfile, previewAsCustomer = false }) => {
|
|
const { user } = useAuth();
|
|
const tier = getUserTier(userProfile);
|
|
const policy = TIER_POLICIES[tier];
|
|
|
|
// Initialize state from existing profile if editing
|
|
const getInitialState = (): WizardState => {
|
|
if (editingProfile) {
|
|
const config = editingProfile.strategy_config;
|
|
const passRatio = config?.execution?.minRulePassRatio || 1.0;
|
|
const style = RISK_STYLE_TEMPLATES.find(t => t.minRulePassRatio === passRatio) || RISK_STYLE_TEMPLATES[1];
|
|
|
|
let hours: WizardState['hours'] = '24/7';
|
|
const sessions = config?.rules?.find((r: any) => r.ruleId === 'SessionRule')?.params?.sessions;
|
|
if (sessions === 'LDN,NY') hours = 'London + New York';
|
|
if (sessions === 'TOK,SYD') hours = 'Asia only';
|
|
|
|
return {
|
|
step: 1,
|
|
riskStyle: style,
|
|
assets: String(editingProfile.symbols).split(','),
|
|
capital: editingProfile.allocated_capital,
|
|
profitTarget: config?.riskLimits?.dailyProfitTargetUsd || 100,
|
|
hours
|
|
};
|
|
}
|
|
return {
|
|
step: 1,
|
|
riskStyle: null,
|
|
assets: ['BTC/USDT'],
|
|
capital: 1000,
|
|
profitTarget: 100,
|
|
hours: '24/7'
|
|
};
|
|
};
|
|
|
|
const [state, setState] = useState<WizardState>(getInitialState());
|
|
const [loading, setLoading] = useState(false);
|
|
const [showBacktest, setShowBacktest] = useState(false);
|
|
const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
|
|
|
|
const next = () => setState(s => ({ ...s, step: s.step + 1 }));
|
|
const back = () => setState(s => ({ ...s, step: s.step - 1 }));
|
|
|
|
const handleSave = async () => {
|
|
if (!user || !state.riskStyle) return;
|
|
setLoading(true);
|
|
|
|
const strategy_config = buildStrategyConfig(state);
|
|
|
|
const payload = {
|
|
name: editingProfile?.name || `${state.riskStyle.label.split(' ')[1]} Bot (${state.assets.join(',')})`,
|
|
user_id: user.id,
|
|
allocated_capital: state.capital,
|
|
risk_per_trade_percent: state.riskStyle.riskPerTrade,
|
|
symbols: state.assets.join(','),
|
|
is_active: editingProfile ? editingProfile.is_active : false, // Preserve status if editing
|
|
strategy_config
|
|
};
|
|
|
|
let result;
|
|
if (editingProfile) {
|
|
result = await updateTradeProfile(editingProfile.id, payload).then(() => ({ error: null as any })).catch((error) => ({ error }));
|
|
} else {
|
|
result = await createTradeProfile(payload).then(() => ({ error: null as any })).catch((error) => ({ error }));
|
|
}
|
|
|
|
setLoading(false);
|
|
|
|
if (result.error) {
|
|
alert('Error saving strategy: ' + result.error.message);
|
|
} else {
|
|
onComplete();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-3xl mx-auto py-8 px-4">
|
|
{/* Progress Bar */}
|
|
<div className="flex justify-between mb-12 relative">
|
|
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-zinc-800 -translate-y-1/2 z-0" />
|
|
{[1, 2, 3, 4, 5].map(i => (
|
|
<div
|
|
key={i}
|
|
className={`relative z-10 w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all duration-300 ${state.step >= i ? 'bg-[#00ff88] text-black scale-110' : 'bg-zinc-800 text-zinc-500 border border-zinc-700'
|
|
}`}
|
|
>
|
|
{state.step > i ? <CheckCircle2 size={18} /> : i}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Step 1: Risk Style */}
|
|
{state.step === 1 && (
|
|
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
<div className="text-center mb-8">
|
|
<h2 className="text-2xl font-bold text-white mb-2">How should this bot behave?</h2>
|
|
<p className="text-zinc-400 text-sm">Select a pre-configured risk style. High-frequency options seek more opportunities but require more flexibility.</p>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{RISK_STYLE_TEMPLATES.map(style => {
|
|
const isLocked = !isFeatureAllowed(tier, 'risk_style', style.id);
|
|
return (
|
|
<div key={style.id} className="relative">
|
|
<button
|
|
disabled={isLocked}
|
|
onClick={() => setState({ ...state, riskStyle: style })}
|
|
className={`w-full p-5 rounded-2xl border-2 text-left transition-all ${isLocked ? 'opacity-40 cursor-not-allowed grayscale' : 'hover:scale-[1.01] active:scale-[0.99]'
|
|
} ${state.riskStyle?.id === style.id
|
|
? 'border-[#00ff88] bg-[#00ff88]/5'
|
|
: 'border-white/5 bg-zinc-900/50 hover:border-white/10'
|
|
}`}
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${style.id === 'safe' ? 'bg-blue-500/10 text-blue-400' :
|
|
style.id === 'balanced' ? 'bg-[#00ff88]/10 text-[#00ff88]' : 'bg-orange-500/10 text-orange-400'
|
|
}`}>
|
|
{style.id === 'safe' ? <ShieldCheck size={24} /> : style.id === 'balanced' ? <Scale size={24} /> : <Zap size={24} />}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-lg font-bold text-white flex items-center gap-2">
|
|
{style.label}
|
|
{isLocked && <Lock size={14} className="text-zinc-500" />}
|
|
</span>
|
|
<span className="text-[10px] uppercase tracking-widest font-black opacity-40">{style.tradeFrequency}</span>
|
|
</div>
|
|
<p className="text-sm text-zinc-400 leading-relaxed">{style.description}</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
{isLocked && (
|
|
<div className="absolute top-4 right-4 text-[9px] font-black uppercase text-amber-500/80 tracking-tighter bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">
|
|
Pro/Elite Only
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="flex justify-end pt-4">
|
|
<button
|
|
disabled={!state.riskStyle}
|
|
onClick={next}
|
|
className="flex items-center gap-2 bg-[#00ff88] text-black px-8 py-3 rounded-xl font-bold hover:brightness-110 transition-all disabled:opacity-30"
|
|
>
|
|
Continue <ChevronRight size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Assets & Capital */}
|
|
{state.step === 2 && (
|
|
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
<div className="text-center">
|
|
<h2 className="text-2xl font-bold text-white mb-2">Assets & Capital</h2>
|
|
<p className="text-zinc-400 text-sm">Define what to trade and how much capital to use.</p>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div className="space-y-3">
|
|
<label className="text-xs font-bold text-zinc-500 uppercase tracking-widest">Select Trading Assets</label>
|
|
<div className="flex flex-wrap gap-3">
|
|
{ASSETS.map(asset => (
|
|
<button
|
|
key={asset.id}
|
|
onClick={() => {
|
|
const newAssets = state.assets.includes(asset.id)
|
|
? state.assets.filter(a => a !== asset.id)
|
|
: [...state.assets, asset.id];
|
|
if (newAssets.length > 0) setState({ ...state, assets: newAssets });
|
|
}}
|
|
className={`px-5 py-3 rounded-xl border-2 font-bold text-sm transition-all ${state.assets.includes(asset.id)
|
|
? 'border-[#00ff88] bg-[#00ff88]/10 text-white'
|
|
: 'border-white/5 bg-zinc-900 text-zinc-500 hover:border-white/10'
|
|
}`}
|
|
>
|
|
{asset.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-3">
|
|
<label className="text-xs font-bold text-zinc-500 uppercase tracking-widest">Capital Allocation</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-4 flex items-center pointer-events-none text-zinc-500">
|
|
<DollarSign size={16} />
|
|
</div>
|
|
<input
|
|
type="number"
|
|
value={state.capital}
|
|
onChange={e => setState({ ...state, capital: Number(e.target.value) })}
|
|
className="w-full bg-zinc-900 border-2 border-white/5 rounded-xl py-4 pl-10 pr-4 text-white font-bold outline-none focus:border-[#00ff88]/50 transition-all"
|
|
/>
|
|
</div>
|
|
<p className="text-[10px] text-zinc-500">Total USD balance this bot is allowed to manage.</p>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<label className="text-xs font-bold text-zinc-500 uppercase tracking-widest flex items-center justify-between">
|
|
Daily Profit Target
|
|
{!isFeatureAllowed(tier, 'profit_target', policy.maxDailyProfitTargetUsd + 1) && (
|
|
<span className="text-[9px] text-amber-500 flex items-center gap-1"><Lock size={10} /> Limited to ${policy.maxDailyProfitTargetUsd}</span>
|
|
)}
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-4 flex items-center pointer-events-none text-zinc-500">
|
|
<Target size={16} />
|
|
</div>
|
|
<input
|
|
type="number"
|
|
disabled={tier === 'free'}
|
|
value={state.profitTarget}
|
|
onChange={e => {
|
|
const val = Number(e.target.value);
|
|
if (isFeatureAllowed(tier, 'profit_target', val)) {
|
|
setState({ ...state, profitTarget: val });
|
|
}
|
|
}}
|
|
className={`w-full bg-zinc-900 border-2 border-white/5 rounded-xl py-4 pl-10 pr-4 text-white font-bold outline-none focus:border-[#00ff88]/50 transition-all ${tier === 'free' ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2 items-start mt-1">
|
|
<Info size={12} className="text-[#00ff88] mt-0.5 shrink-0" />
|
|
<p className="text-[10px] text-zinc-500 italic">Once this profit is reached, the bot automatically pauses for the day to lock in gains.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button onClick={back} className="flex items-center gap-2 text-zinc-500 hover:text-white font-bold px-6 transition-all">
|
|
<ChevronLeft size={18} /> Back
|
|
</button>
|
|
<button onClick={next} className="flex items-center gap-2 bg-[#00ff88] text-black px-8 py-3 rounded-xl font-bold hover:brightness-110 transition-all">
|
|
Continue <ChevronRight size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Trading Hours */}
|
|
{state.step === 3 && (
|
|
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
<div className="text-center">
|
|
<h2 className="text-2xl font-bold text-white mb-2">Trading Hours</h2>
|
|
<p className="text-zinc-400 text-sm">When should the bot look for signals?</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{(['24/7', 'London + New York', 'Asia only'] as const).map(option => (
|
|
<button
|
|
key={option}
|
|
onClick={() => setState({ ...state, hours: option })}
|
|
className={`p-6 rounded-2xl border-2 text-left transition-all ${state.hours === option
|
|
? 'border-[#00ff88] bg-[#00ff88]/5'
|
|
: 'border-white/5 bg-zinc-900/50 hover:border-white/10'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${state.hours === option ? 'bg-[#00ff88]/10 text-[#00ff88]' : 'bg-zinc-800 text-zinc-500'}`}>
|
|
<Clock size={20} />
|
|
</div>
|
|
<div>
|
|
<span className="block font-bold text-white text-lg">{option}</span>
|
|
<span className="text-xs text-zinc-500">
|
|
{option === '24/7' ? 'Universal coverage, trades any time signals appear.' :
|
|
option === 'London + New York' ? 'Focuses on the most liquid market overlap (07:00 - 21:00 UTC).' :
|
|
'Optimized for Tokoyo and Sydney sessions.'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button onClick={back} className="flex items-center gap-2 text-zinc-500 hover:text-white font-bold px-6 transition-all">
|
|
<ChevronLeft size={18} /> Back
|
|
</button>
|
|
<button onClick={next} className="flex items-center gap-2 bg-[#00ff88] text-black px-8 py-3 rounded-xl font-bold hover:brightness-110 transition-all">
|
|
Continue <ChevronRight size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 4: Safety Confirmation */}
|
|
{state.step === 4 && (
|
|
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
<div className="text-center">
|
|
<h2 className="text-2xl font-bold text-white mb-2">Safety Confirmation</h2>
|
|
<p className="text-zinc-400 text-sm">Review the built-in safeguards protecting your capital.</p>
|
|
</div>
|
|
|
|
<div className="bg-zinc-900/80 rounded-2xl border border-white/5 overflow-hidden">
|
|
<div className="p-6 space-y-4">
|
|
<div className="flex items-center justify-between py-3 border-b border-white/5">
|
|
<span className="text-zinc-500 text-sm font-medium">Auto-Protective Stop Loss</span>
|
|
<span className="text-[#00ff88] text-sm font-bold">Enabled (Mandatory)</span>
|
|
</div>
|
|
<div className="flex items-center justify-between py-3 border-b border-white/5">
|
|
<span className="text-zinc-500 text-sm font-medium">Position Sizing Limit</span>
|
|
<span className="text-white text-sm font-bold font-mono">Max {state.riskStyle?.riskPerTrade}% per trade</span>
|
|
</div>
|
|
<div className="flex items-center justify-between py-3 border-b border-white/5">
|
|
<span className="text-zinc-500 text-sm font-medium">Daily Recovery Halt</span>
|
|
<span className="text-rose-400 text-sm font-bold font-mono">-${Math.floor(state.capital * 0.05)} (5%)</span>
|
|
</div>
|
|
<div className="flex items-center justify-between py-3">
|
|
<span className="text-zinc-500 text-sm font-medium">Market Volatility Guard</span>
|
|
<span className="text-blue-400 text-sm font-bold">Smart ATR Check</span>
|
|
</div>
|
|
</div>
|
|
<div className="bg-amber-500/10 border-t border-amber-500/20 p-5 flex gap-4">
|
|
<AlertTriangle className="text-amber-500 shrink-0" size={20} />
|
|
<p className="text-xs text-amber-500/90 leading-relaxed font-medium">
|
|
Risk management and safety rules are coded into the engine core. These cannot be disabled or bypassed, ensuring your bot always operates within safety boundaries.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button onClick={back} className="flex items-center gap-2 text-zinc-500 hover:text-white font-bold px-6 transition-all">
|
|
<ChevronLeft size={18} /> Back
|
|
</button>
|
|
<button onClick={next} className="flex items-center gap-2 bg-[#00ff88] text-black px-8 py-3 rounded-xl font-bold hover:brightness-110 transition-all">
|
|
I Understand <ChevronRight size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 5: Review & Create */}
|
|
{state.step === 5 && (
|
|
<div className="space-y-8 animate-in fade-in zoom-in-95 duration-500">
|
|
<div className="text-center">
|
|
<h2 className="text-3xl font-bold text-white mb-2">Ready to Deploy</h2>
|
|
<p className="text-zinc-400 text-sm">Verify your bot strategy one last time.</p>
|
|
</div>
|
|
|
|
<div className="bg-[#00ff88]/5 rounded-3xl border-2 border-[#00ff88]/20 p-8 space-y-6 relative overflow-hidden">
|
|
{/* Decorative background element */}
|
|
<div className="absolute -top-24 -right-24 w-64 h-64 bg-[#00ff88]/10 blur-[100px] rounded-full pointer-events-none" />
|
|
|
|
<div className="flex items-center gap-6">
|
|
<div className="w-20 h-20 rounded-2xl bg-[#00ff88] text-black flex items-center justify-center">
|
|
{state.riskStyle?.id === 'safe' ? <ShieldCheck size={40} /> : state.riskStyle?.id === 'balanced' ? <Scale size={40} /> : <Zap size={40} />}
|
|
</div>
|
|
<div>
|
|
<h3 className="text-2xl font-bold text-white">{state.riskStyle?.label} Bot</h3>
|
|
<div className="flex gap-2 mt-1">
|
|
{state.assets.map(a => (
|
|
<span key={a} className="bg-white/10 px-2 py-0.5 rounded text-[10px] font-black text-white/80">{a.split('/')[0]}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-8 pt-4">
|
|
<div className="space-y-1">
|
|
<span className="text-zinc-500 text-[10px] uppercase font-black tracking-widest">Target Capital</span>
|
|
<div className="text-xl font-bold text-white flex items-baseline gap-1">
|
|
<span className="text-sm opacity-50 opacity-40">$</span>{state.capital}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<span className="text-zinc-500 text-[10px] uppercase font-black tracking-widest">Profit Threshold</span>
|
|
<div className="text-xl font-bold text-[#00ff88] flex items-baseline gap-1">
|
|
<span className="text-sm opacity-40">$</span>{state.profitTarget}<span className="text-[10px] opacity-60 ml-1">/day</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<span className="text-zinc-500 text-[10px] uppercase font-black tracking-widest">Risk Level</span>
|
|
<div className="text-xl font-bold text-white">{(state.riskStyle?.riskPerTrade || 0)}% <span className="text-xs opacity-50 font-normal">per trade</span></div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<span className="text-zinc-500 text-[10px] uppercase font-black tracking-widest">Active Schedule</span>
|
|
<div className="text-xl font-bold text-white">{state.hours}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-6 border-t border-white/5 flex gap-4 items-center">
|
|
<div className="w-12 h-12 rounded-full border border-zinc-800 flex items-center justify-center text-zinc-600">
|
|
<Wallet size={20} />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-zinc-400">Bots are created in <span className="text-white font-bold">PAUSED</span> mode by default. You must manually enable trading from your dashboard.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-4">
|
|
<button onClick={back} className="flex items-center gap-2 text-zinc-500 hover:text-white font-bold px-6 transition-all">
|
|
<ChevronLeft size={18} /> Back
|
|
</button>
|
|
<div className="flex items-center gap-3">
|
|
{!backtestGateLoading && backtestEnabled && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowBacktest((prev) => !prev)}
|
|
className="flex items-center gap-2 bg-white/10 text-white px-5 py-3 rounded-xl font-black text-xs uppercase tracking-wider hover:bg-white/20 transition-all"
|
|
>
|
|
{showBacktest ? 'Hide Backtest' : 'Run Backtest'}
|
|
</button>
|
|
)}
|
|
<button
|
|
disabled={loading}
|
|
onClick={handleSave}
|
|
className="group relative flex items-center gap-3 bg-[#00ff88] text-black px-12 py-4 rounded-2xl font-black text-lg hover:brightness-110 hover:scale-[1.02] active:scale-[0.98] transition-all shadow-2xl shadow-[#00ff88]/20 disabled:opacity-50"
|
|
>
|
|
{loading ? 'Processing...' : (
|
|
<>
|
|
DEPLOY STRATEGY
|
|
<ChevronRight size={20} className="group-hover:translate-x-1 transition-transform" />
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{!backtestGateLoading && backtestEnabled && showBacktest && (
|
|
<div className="pt-6">
|
|
<BacktestRunnerPanel
|
|
strategyConfig={buildStrategyConfig(state)}
|
|
symbols={state.assets}
|
|
initialCapitalUsd={state.capital}
|
|
title="Pre-Deploy Backtest"
|
|
onClose={() => setShowBacktest(false)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|