diff --git a/web/src/components/StrategyWizard.tsx b/web/src/components/StrategyWizard.tsx index 6ea880e..c8e1e4b 100644 --- a/web/src/components/StrategyWizard.tsx +++ b/web/src/components/StrategyWizard.tsx @@ -1,19 +1,19 @@ -import React, { useState } from 'react'; -import { - ShieldCheck, - Scale, - Zap, - DollarSign, - Clock, - ChevronRight, - ChevronLeft, - CheckCircle2, - AlertTriangle, - Info, - Wallet, - Target, - Lock -} from 'lucide-react'; +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'; @@ -24,22 +24,22 @@ import { createTradeProfile, updateTradeProfile } from '../lib/profileApi'; import { Button } from './ui/button'; import { Card } from './ui/card'; import { Input } from './ui/input'; - -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)' }, -]; - + +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', @@ -75,89 +75,89 @@ const buildStrategyConfig = (state: WizardState) => ({ 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 { 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(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 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 - }; - + + 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 ( + + setLoading(false); + + if (result.error) { + alert('Error saving strategy: ' + result.error.message); + } else { + onComplete(); + } + }; + + return (
{/* Progress Bar */}
@@ -168,62 +168,61 @@ export const StrategyWizard: React.FC<{ className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold transition-all duration-300 ${state.step >= i ? 'scale-110 bg-[var(--primary)] text-[var(--primary-foreground)]' : 'border border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)]' }`} > - {state.step > i ? : i} -
- ))} -
- - {/* Step 1: Risk Style */} - {state.step === 1 && ( -
+ {state.step > i ? : i} +
+ ))} + + + {/* Step 1: Risk Style */} + {state.step === 1 && ( +

How should this bot behave?

Select a pre-configured risk style. High-frequency options seek more opportunities but require more flexibility.

-
-
- {RISK_STYLE_TEMPLATES.map(style => { - const isLocked = !isFeatureAllowed(tier, 'risk_style', style.id); - return ( -
- - {isLocked && ( -
- Pro/Elite Only -
- )} -
- ); - })} -
-
+ + {isLocked && ( +
+ Pro/Elite Only +
+ )} +
+ ); + })} +
+
-
- - )} - - {/* Step 2: Assets & Capital */} - {state.step === 2 && ( -
+
+ + )} + + {/* Step 2: Assets & Capital */} + {state.step === 2 && ( +

Assets & Capital

Define what to trade and how much capital to use.

-
- -
-
+
+ +
+
-
- {ASSETS.map(asset => ( +
+ {ASSETS.map(asset => ( - ))} -
-
- -
-
+ ))} +
+
+ +
+
@@ -284,109 +283,108 @@ export const StrategyWizard: React.FC<{ />

Total USD balance this bot is allowed to manage.

-
- -
+
+ +
-
+ Daily Profit Target + {!isFeatureAllowed(tier, 'profit_target', policy.maxDailyProfitTargetUsd + 1) && ( + Limited to ${policy.maxDailyProfitTargetUsd} + )} + +
- -
+ +
{ - const val = Number(e.target.value); - if (isFeatureAllowed(tier, 'profit_target', val)) { - setState({ ...state, profitTarget: val }); - } - }} + 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={`h-14 pl-10 font-bold ${tier === 'free' ? 'cursor-not-allowed opacity-50' : ''}`} /> -
-
+
+

Once this profit is reached, the bot automatically pauses for the day to lock in gains.

-
-
-
-
- -
+
+
+
+
+ +
-
-
- )} - - {/* Step 3: Trading Hours */} - {state.step === 3 && ( -
-
+
+
+ )} + + {/* Step 3: Trading Hours */} + {state.step === 3 && ( +
+

Trading Hours

When should the bot look for signals?

-
- -
- {(['24/7', 'London + New York', 'Asia only'] as const).map(option => ( - + ))} -
- -
+
+ +
-
-
- )} - - {/* Step 4: Safety Confirmation */} - {state.step === 4 && ( -
-
+
+
+ )} + + {/* Step 4: Safety Confirmation */} + {state.step === 4 && ( +
+

Safety Confirmation

Review the built-in safeguards protecting your capital.

-
- +
+ -
+
Auto-Protective Stop Loss Enabled (Mandatory) @@ -397,36 +395,36 @@ export const StrategyWizard: React.FC<{
Daily Recovery Halt - -${Math.floor(state.capital * 0.05)} (5%) -
-
+ -${Math.floor(state.capital * 0.05)} (5%) +
+
Market Volatility Guard - Smart ATR Check -
-
-
- -

- 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. + Smart ATR Check +

+
+
+ +

+ 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.

- -
+ +
-
-
- )} - - {/* Step 5: Review & Create */} - {state.step === 5 && ( -
-
+
+
+ )} + + {/* Step 5: Review & Create */} + {state.step === 5 && ( +
+

Ready to Deploy

Verify your bot strategy one last time.

@@ -444,41 +442,41 @@ export const StrategyWizard: React.FC<{ ))}
- - -
-
+
+ +
+
Target Capital
- ${state.capital} -
-
-
+ ${state.capital} +
+
+
Profit Threshold
- ${state.profitTarget}/day -
-
-
+ ${state.profitTarget}/day +
+
+
Risk Level
{(state.riskStyle?.riskPerTrade || 0)}% per trade
-
-
+
+
Active Schedule
{state.hours}
-
- - + + +
- -
-
+ +
+

Bots are created in PAUSED mode by default. You must manually enable trading from your dashboard.

- +
+ ))} + + + {/* 5. Health Diagnostic (Education Layer) */} +
+

- {explanation.reason} -

- {isExpanded && explanation.recommendation && ( -
+ {isExpanded && explanation.recommendation && ( +
+ borderRadius: '12px' + }}>
- Optimization -
+ Optimization +

{explanation.recommendation}

-
- )} - - - - {/* 6. Action */} -
+
+ )} + + + + {/* 6. Action */} +
-
- - - - ); -}; - + .action-btn-hover:hover { + filter: brightness(1.1); + transform: scale(1.02); + } + `} + + ); +}; + export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewAsCustomer?: boolean }> = ({ botState, alerts = [], previewAsCustomer = false }) => { const { user, profile: userProfile } = useAuth(); - const [profiles, setProfiles] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [profiles, setProfiles] = useState([]); + const [isLoading, setIsLoading] = useState(true); const [showWizard, setShowWizard] = useState(false); const [editingProfile, setEditingProfile] = useState(null); const [expandedExplanations, setExpandedExplanations] = useState>({}); @@ -263,7 +263,7 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA const tier = getUserTier(userProfile); const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer }); - + const fetchProfiles = async () => { if (!user) return; setIsLoading(true); @@ -271,13 +271,13 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA setProfiles(data || []); setIsLoading(false); }; - - useEffect(() => { - fetchProfiles(); - window.addEventListener('profiles-updated', fetchProfiles); - return () => window.removeEventListener('profiles-updated', fetchProfiles); - }, [user]); - + + useEffect(() => { + fetchProfiles(); + window.addEventListener('profiles-updated', fetchProfiles); + return () => window.removeEventListener('profiles-updated', fetchProfiles); + }, [user]); + const toggleBot = async (profile: any) => { try { await setTradeProfileActive(profile.id, !profile.is_active); @@ -296,11 +296,11 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA // existing UI remains silent on delete failure } }; - - if (showWizard) { - return ( -
-
+ + if (showWizard) { + return ( +
+
-
+
-
- ); - } - + }} + /> +
+ ); + } + return (
@@ -353,67 +353,67 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
- - {/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */} - {(() => { - const activeSymbols = [...new Set(profiles.flatMap(p => p.symbols?.split(',').map((s: string) => s.trim()) || []))]; - const recentAlerts = [...alerts].reverse().slice(0, 5); - const symbolVolatility = activeSymbols - .filter(s => botState?.symbols?.[s]) - .map(s => ({ symbol: s, change: botState.symbols[s].change24h || 0 })) - .sort((a, b) => Math.abs(b.change) - Math.abs(a.change)); - - return ( -
- {/* Recent Activity */} + + {/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */} + {(() => { + const activeSymbols = [...new Set(profiles.flatMap(p => p.symbols?.split(',').map((s: string) => s.trim()) || []))]; + const recentAlerts = [...alerts].reverse().slice(0, 5); + const symbolVolatility = activeSymbols + .filter(s => botState?.symbols?.[s]) + .map(s => ({ symbol: s, change: botState.symbols[s].change24h || 0 })) + .sort((a, b) => Math.abs(b.change) - Math.abs(a.change)); + + return ( +
+ {/* Recent Activity */}
-
+
Recent Activity -
-
- {recentAlerts.map((alert, i) => { +
+
+ {recentAlerts.map((alert, i) => { const mins = Math.floor((Date.now() - alert.timestamp) / 60000); - const timeAgo = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h ago`; - return ( + const timeAgo = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h ago`; + return (
{alert.symbol} {alert.message} {timeAgo} -
- ); - })} +
+ ); + })} {recentAlerts.length === 0 &&
No activity yet...
} -
-
- - {/* Symbol-Specific Volatility */} +
+
+ + {/* Symbol-Specific Volatility */}
-
+
Your Markets (24h) -
-
- {symbolVolatility.map(({ symbol, change }) => ( +
+
+ {symbolVolatility.map(({ symbol, change }) => (
- {symbol} -
+ {symbol} +
= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', borderRadius: '99px' }} />
= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', minWidth: '55px', textAlign: 'right' }}> - {change >= 0 ? '+' : ''}{change.toFixed(2)}% - -
-
- ))} + {change >= 0 ? '+' : ''}{change.toFixed(2)}% + +
+
+ ))} {symbolVolatility.length === 0 &&
Deploy a strategy to see its market data
} -
-
-
- ); - })()} +
+ + + ); + })()}
setExpandedExplanations(prev => ({ ...prev, [id]: !prev[id] }))} /> ))} - - {profiles.length === 0 && !isLoading && ( -
+ borderRadius: '40px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center' + }}>

No strategies yet

Create your first strategy to start monitoring markets and testing automated execution.

@@ -474,16 +474,16 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA > GET STARTED -
- )} -
- - - - ); -}; + + )} + + + + + ); +}; diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx index 297b4cf..5525f42 100644 --- a/web/src/views/SimpleView.tsx +++ b/web/src/views/SimpleView.tsx @@ -1006,15 +1006,14 @@ export function SimpleView() {

-
- - +