fix(web): convert <Button> picker cards to native <button> (UI audit #5)
The @bytelyst/ui Button primitive applies whitespace-nowrap + fixed
h-{size} which collapses multi-line stacked content (Pattern A in
docs/ui/UI_AUDIT.md). Affected sites use Button as a card-shaped
option/action picker with stacked title + description spans.
Converted to native <button> + new .card-button utility class:
- StrategyWizard:188 — risk style picker (3 cards)
- StrategyWizard:342 — trading hours picker
- SimpleView:1009 — "new short-term buy plan" card
- SimpleView:1028 — "manage existing holding" card
- MyStrategiesTab:153 — diagnostic accordion toggle
Adds layout-fixes.css §25 .card-button — resets button defaults,
preserves focus-visible ring tied to --bl-focus-ring/--bl-accent.
Skipped: ChatControl:543 (FAB) — single-icon child, not affected.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
1807dc0d30
commit
67c9ecb589
@ -1,19 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Scale,
|
Scale,
|
||||||
Zap,
|
Zap,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Clock,
|
Clock,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Info,
|
Info,
|
||||||
Wallet,
|
Wallet,
|
||||||
Target,
|
Target,
|
||||||
Lock
|
Lock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { RISK_STYLE_TEMPLATES } from '../lib/RiskStyleTemplates';
|
import { RISK_STYLE_TEMPLATES } from '../lib/RiskStyleTemplates';
|
||||||
import type { RiskStyleTemplate } from '../lib/RiskStyleTemplates';
|
import type { RiskStyleTemplate } from '../lib/RiskStyleTemplates';
|
||||||
import { getUserTier, TIER_POLICIES, isFeatureAllowed } from '../lib/TierPolicy';
|
import { getUserTier, TIER_POLICIES, isFeatureAllowed } from '../lib/TierPolicy';
|
||||||
@ -24,22 +24,22 @@ import { createTradeProfile, updateTradeProfile } from '../lib/profileApi';
|
|||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Card } from './ui/card';
|
import { Card } from './ui/card';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
|
|
||||||
interface WizardState {
|
interface WizardState {
|
||||||
step: number;
|
step: number;
|
||||||
riskStyle: RiskStyleTemplate | null;
|
riskStyle: RiskStyleTemplate | null;
|
||||||
assets: string[];
|
assets: string[];
|
||||||
capital: number;
|
capital: number;
|
||||||
profitTarget: number;
|
profitTarget: number;
|
||||||
hours: '24/7' | 'London + New York' | 'Asia only';
|
hours: '24/7' | 'London + New York' | 'Asia only';
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
{ id: 'BTC/USDT', label: 'Bitcoin (BTC)' },
|
{ id: 'BTC/USDT', label: 'Bitcoin (BTC)' },
|
||||||
{ id: 'ETH/USDT', label: 'Ethereum (ETH)' },
|
{ id: 'ETH/USDT', label: 'Ethereum (ETH)' },
|
||||||
{ id: 'SOL/USDT', label: 'Solana (SOL)' },
|
{ id: 'SOL/USDT', label: 'Solana (SOL)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SESSION_MAP = {
|
const SESSION_MAP = {
|
||||||
'24/7': 'LDN,NY,TOK,SYD',
|
'24/7': 'LDN,NY,TOK,SYD',
|
||||||
'London + New York': 'LDN,NY',
|
'London + New York': 'LDN,NY',
|
||||||
@ -75,89 +75,89 @@ const buildStrategyConfig = (state: WizardState) => ({
|
|||||||
entryMode: 'both'
|
entryMode: 'both'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StrategyWizard: React.FC<{
|
export const StrategyWizard: React.FC<{
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
editingProfile?: any;
|
editingProfile?: any;
|
||||||
profile?: any;
|
profile?: any;
|
||||||
previewAsCustomer?: boolean;
|
previewAsCustomer?: boolean;
|
||||||
}> = ({ onComplete, editingProfile, profile: userProfile, previewAsCustomer = false }) => {
|
}> = ({ onComplete, editingProfile, profile: userProfile, previewAsCustomer = false }) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const tier = getUserTier(userProfile);
|
const tier = getUserTier(userProfile);
|
||||||
const policy = TIER_POLICIES[tier];
|
const policy = TIER_POLICIES[tier];
|
||||||
|
|
||||||
// Initialize state from existing profile if editing
|
// Initialize state from existing profile if editing
|
||||||
const getInitialState = (): WizardState => {
|
const getInitialState = (): WizardState => {
|
||||||
if (editingProfile) {
|
if (editingProfile) {
|
||||||
const config = editingProfile.strategy_config;
|
const config = editingProfile.strategy_config;
|
||||||
const passRatio = config?.execution?.minRulePassRatio || 1.0;
|
const passRatio = config?.execution?.minRulePassRatio || 1.0;
|
||||||
const style = RISK_STYLE_TEMPLATES.find(t => t.minRulePassRatio === passRatio) || RISK_STYLE_TEMPLATES[1];
|
const style = RISK_STYLE_TEMPLATES.find(t => t.minRulePassRatio === passRatio) || RISK_STYLE_TEMPLATES[1];
|
||||||
|
|
||||||
let hours: WizardState['hours'] = '24/7';
|
let hours: WizardState['hours'] = '24/7';
|
||||||
const sessions = config?.rules?.find((r: any) => r.ruleId === 'SessionRule')?.params?.sessions;
|
const sessions = config?.rules?.find((r: any) => r.ruleId === 'SessionRule')?.params?.sessions;
|
||||||
if (sessions === 'LDN,NY') hours = 'London + New York';
|
if (sessions === 'LDN,NY') hours = 'London + New York';
|
||||||
if (sessions === 'TOK,SYD') hours = 'Asia only';
|
if (sessions === 'TOK,SYD') hours = 'Asia only';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: 1,
|
step: 1,
|
||||||
riskStyle: style,
|
riskStyle: style,
|
||||||
assets: String(editingProfile.symbols).split(','),
|
assets: String(editingProfile.symbols).split(','),
|
||||||
capital: editingProfile.allocated_capital,
|
capital: editingProfile.allocated_capital,
|
||||||
profitTarget: config?.riskLimits?.dailyProfitTargetUsd || 100,
|
profitTarget: config?.riskLimits?.dailyProfitTargetUsd || 100,
|
||||||
hours
|
hours
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
step: 1,
|
step: 1,
|
||||||
riskStyle: null,
|
riskStyle: null,
|
||||||
assets: ['BTC/USDT'],
|
assets: ['BTC/USDT'],
|
||||||
capital: 1000,
|
capital: 1000,
|
||||||
profitTarget: 100,
|
profitTarget: 100,
|
||||||
hours: '24/7'
|
hours: '24/7'
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const [state, setState] = useState<WizardState>(getInitialState());
|
const [state, setState] = useState<WizardState>(getInitialState());
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showBacktest, setShowBacktest] = useState(false);
|
const [showBacktest, setShowBacktest] = useState(false);
|
||||||
const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
|
const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
|
||||||
|
|
||||||
const next = () => setState(s => ({ ...s, step: s.step + 1 }));
|
const next = () => setState(s => ({ ...s, step: s.step + 1 }));
|
||||||
const back = () => setState(s => ({ ...s, step: s.step - 1 }));
|
const back = () => setState(s => ({ ...s, step: s.step - 1 }));
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!user || !state.riskStyle) return;
|
if (!user || !state.riskStyle) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const strategy_config = buildStrategyConfig(state);
|
const strategy_config = buildStrategyConfig(state);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: editingProfile?.name || `${state.riskStyle.label.split(' ')[1]} Bot (${state.assets.join(',')})`,
|
name: editingProfile?.name || `${state.riskStyle.label.split(' ')[1]} Bot (${state.assets.join(',')})`,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
allocated_capital: state.capital,
|
allocated_capital: state.capital,
|
||||||
risk_per_trade_percent: state.riskStyle.riskPerTrade,
|
risk_per_trade_percent: state.riskStyle.riskPerTrade,
|
||||||
symbols: state.assets.join(','),
|
symbols: state.assets.join(','),
|
||||||
is_active: editingProfile ? editingProfile.is_active : false, // Preserve status if editing
|
is_active: editingProfile ? editingProfile.is_active : false, // Preserve status if editing
|
||||||
strategy_config
|
strategy_config
|
||||||
};
|
};
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
if (editingProfile) {
|
if (editingProfile) {
|
||||||
result = await updateTradeProfile(editingProfile.id, payload).then(() => ({ error: null as any })).catch((error) => ({ error }));
|
result = await updateTradeProfile(editingProfile.id, payload).then(() => ({ error: null as any })).catch((error) => ({ error }));
|
||||||
} else {
|
} else {
|
||||||
result = await createTradeProfile(payload).then(() => ({ error: null as any })).catch((error) => ({ error }));
|
result = await createTradeProfile(payload).then(() => ({ error: null as any })).catch((error) => ({ error }));
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
alert('Error saving strategy: ' + result.error.message);
|
alert('Error saving strategy: ' + result.error.message);
|
||||||
} else {
|
} else {
|
||||||
onComplete();
|
onComplete();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-4 py-8">
|
<div className="mx-auto max-w-3xl px-4 py-8">
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
<div className="relative mb-12 flex justify-between">
|
<div className="relative mb-12 flex justify-between">
|
||||||
@ -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)]'
|
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 ? <CheckCircle2 size={18} /> : i}
|
{state.step > i ? <CheckCircle2 size={18} /> : i}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step 1: Risk Style */}
|
{/* Step 1: Risk Style */}
|
||||||
{state.step === 1 && (
|
{state.step === 1 && (
|
||||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<h2 className={sectionTitleClass}>How should this bot behave?</h2>
|
<h2 className={sectionTitleClass}>How should this bot behave?</h2>
|
||||||
<p className={sectionDescriptionClass}>Select a pre-configured risk style. High-frequency options seek more opportunities but require more flexibility.</p>
|
<p className={sectionDescriptionClass}>Select a pre-configured risk style. High-frequency options seek more opportunities but require more flexibility.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
{RISK_STYLE_TEMPLATES.map(style => {
|
{RISK_STYLE_TEMPLATES.map(style => {
|
||||||
const isLocked = !isFeatureAllowed(tier, 'risk_style', style.id);
|
const isLocked = !isFeatureAllowed(tier, 'risk_style', style.id);
|
||||||
return (
|
return (
|
||||||
<div key={style.id} className="relative">
|
<div key={style.id} className="relative">
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
|
||||||
disabled={isLocked}
|
disabled={isLocked}
|
||||||
onClick={() => setState({ ...state, riskStyle: style })}
|
onClick={() => setState({ ...state, riskStyle: style })}
|
||||||
className={`${optionBaseClass} ${isLocked ? 'cursor-not-allowed opacity-40 grayscale' : 'hover:scale-[1.01] active:scale-[0.99]'
|
className={`card-button ${optionBaseClass} ${isLocked ? 'cursor-not-allowed opacity-40 grayscale' : 'hover:scale-[1.01] active:scale-[0.99]'
|
||||||
} ${state.riskStyle?.id === style.id
|
} ${state.riskStyle?.id === style.id
|
||||||
? optionSelectedClass
|
? optionSelectedClass
|
||||||
: optionIdleClass
|
: optionIdleClass
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<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' :
|
<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-[var(--accent-soft)] text-[var(--accent)]' : 'bg-orange-500/10 text-orange-400'
|
style.id === 'balanced' ? 'bg-[var(--accent-soft)] text-[var(--accent)]' : 'bg-orange-500/10 text-orange-400'
|
||||||
}`}>
|
}`}>
|
||||||
{style.id === 'safe' ? <ShieldCheck size={24} /> : style.id === 'balanced' ? <Scale size={24} /> : <Zap size={24} />}
|
{style.id === 'safe' ? <ShieldCheck size={24} /> : style.id === 'balanced' ? <Scale size={24} /> : <Zap size={24} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<span className="flex items-center gap-2 text-lg font-bold text-[var(--foreground)]">
|
<span className="flex items-center gap-2 text-lg font-bold text-[var(--foreground)]">
|
||||||
{style.label}
|
{style.label}
|
||||||
{isLocked && <Lock size={14} className="text-[var(--muted-foreground)]" />}
|
{isLocked && <Lock size={14} className="text-[var(--muted-foreground)]" />}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] uppercase tracking-widest font-black opacity-40">{style.tradeFrequency}</span>
|
<span className="text-[10px] uppercase tracking-widest font-black opacity-40">{style.tradeFrequency}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-relaxed text-[var(--muted-foreground)]">{style.description}</p>
|
<p className="text-sm leading-relaxed text-[var(--muted-foreground)]">{style.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</button>
|
||||||
{isLocked && (
|
{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">
|
<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
|
Pro/Elite Only
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
<Button
|
<Button
|
||||||
disabled={!state.riskStyle}
|
disabled={!state.riskStyle}
|
||||||
onClick={next}
|
onClick={next}
|
||||||
@ -231,33 +230,33 @@ export const StrategyWizard: React.FC<{
|
|||||||
>
|
>
|
||||||
Continue <ChevronRight size={18} />
|
Continue <ChevronRight size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 2: Assets & Capital */}
|
{/* Step 2: Assets & Capital */}
|
||||||
{state.step === 2 && (
|
{state.step === 2 && (
|
||||||
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className={sectionTitleClass}>Assets & Capital</h2>
|
<h2 className={sectionTitleClass}>Assets & Capital</h2>
|
||||||
<p className={sectionDescriptionClass}>Define what to trade and how much capital to use.</p>
|
<p className={sectionDescriptionClass}>Define what to trade and how much capital to use.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className={labelClass}>Select Trading Assets</label>
|
<label className={labelClass}>Select Trading Assets</label>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{ASSETS.map(asset => (
|
{ASSETS.map(asset => (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
key={asset.id}
|
key={asset.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newAssets = state.assets.includes(asset.id)
|
const newAssets = state.assets.includes(asset.id)
|
||||||
? state.assets.filter(a => a !== asset.id)
|
? state.assets.filter(a => a !== asset.id)
|
||||||
: [...state.assets, asset.id];
|
: [...state.assets, asset.id];
|
||||||
if (newAssets.length > 0) setState({ ...state, assets: newAssets });
|
if (newAssets.length > 0) setState({ ...state, assets: newAssets });
|
||||||
}}
|
}}
|
||||||
className={`rounded-xl border-2 px-5 py-3 text-sm font-bold transition-all ${state.assets.includes(asset.id)
|
className={`rounded-xl border-2 px-5 py-3 text-sm font-bold transition-all ${state.assets.includes(asset.id)
|
||||||
? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--foreground)]'
|
? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--foreground)]'
|
||||||
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border-strong)]'
|
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border-strong)]'
|
||||||
@ -265,12 +264,12 @@ export const StrategyWizard: React.FC<{
|
|||||||
>
|
>
|
||||||
{asset.label}
|
{asset.label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className={labelClass}>Capital Allocation</label>
|
<label className={labelClass}>Capital Allocation</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-4 flex items-center text-[var(--muted-foreground)]">
|
<div className="pointer-events-none absolute inset-y-0 left-4 flex items-center text-[var(--muted-foreground)]">
|
||||||
@ -284,109 +283,108 @@ export const StrategyWizard: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-[var(--muted-foreground)]">Total USD balance this bot is allowed to manage.</p>
|
<p className="text-[10px] text-[var(--muted-foreground)]">Total USD balance this bot is allowed to manage.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="flex items-center justify-between text-xs font-bold uppercase tracking-widest text-[var(--muted-foreground)]">
|
<label className="flex items-center justify-between text-xs font-bold uppercase tracking-widest text-[var(--muted-foreground)]">
|
||||||
Daily Profit Target
|
Daily Profit Target
|
||||||
{!isFeatureAllowed(tier, 'profit_target', policy.maxDailyProfitTargetUsd + 1) && (
|
{!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>
|
<span className="text-[9px] text-amber-500 flex items-center gap-1"><Lock size={10} /> Limited to ${policy.maxDailyProfitTargetUsd}</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-4 flex items-center text-[var(--muted-foreground)]">
|
<div className="pointer-events-none absolute inset-y-0 left-4 flex items-center text-[var(--muted-foreground)]">
|
||||||
<Target size={16} />
|
<Target size={16} />
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={tier === 'free'}
|
disabled={tier === 'free'}
|
||||||
value={state.profitTarget}
|
value={state.profitTarget}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
const val = Number(e.target.value);
|
const val = Number(e.target.value);
|
||||||
if (isFeatureAllowed(tier, 'profit_target', val)) {
|
if (isFeatureAllowed(tier, 'profit_target', val)) {
|
||||||
setState({ ...state, profitTarget: val });
|
setState({ ...state, profitTarget: val });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`h-14 pl-10 font-bold ${tier === 'free' ? 'cursor-not-allowed opacity-50' : ''}`}
|
className={`h-14 pl-10 font-bold ${tier === 'free' ? 'cursor-not-allowed opacity-50' : ''}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-start mt-1">
|
<div className="flex gap-2 items-start mt-1">
|
||||||
<Info size={12} className="mt-0.5 shrink-0 text-[var(--accent)]" />
|
<Info size={12} className="mt-0.5 shrink-0 text-[var(--accent)]" />
|
||||||
<p className="text-[10px] italic text-[var(--muted-foreground)]">Once this profit is reached, the bot automatically pauses for the day to lock in gains.</p>
|
<p className="text-[10px] italic text-[var(--muted-foreground)]">Once this profit is reached, the bot automatically pauses for the day to lock in gains.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-between pt-4">
|
||||||
<Button onClick={back} variant="ghost" className="px-6">
|
<Button onClick={back} variant="ghost" className="px-6">
|
||||||
<ChevronLeft size={18} /> Back
|
<ChevronLeft size={18} /> Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={next} className="px-8">
|
<Button onClick={next} className="px-8">
|
||||||
Continue <ChevronRight size={18} />
|
Continue <ChevronRight size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 3: Trading Hours */}
|
{/* Step 3: Trading Hours */}
|
||||||
{state.step === 3 && (
|
{state.step === 3 && (
|
||||||
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className={sectionTitleClass}>Trading Hours</h2>
|
<h2 className={sectionTitleClass}>Trading Hours</h2>
|
||||||
<p className={sectionDescriptionClass}>When should the bot look for signals?</p>
|
<p className={sectionDescriptionClass}>When should the bot look for signals?</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
{(['24/7', 'London + New York', 'Asia only'] as const).map(option => (
|
{(['24/7', 'London + New York', 'Asia only'] as const).map(option => (
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
|
||||||
key={option}
|
key={option}
|
||||||
onClick={() => setState({ ...state, hours: option })}
|
onClick={() => setState({ ...state, hours: option })}
|
||||||
className={`rounded-2xl border-2 p-6 text-left transition-all ${state.hours === option
|
className={`card-button rounded-2xl border-2 p-6 text-left transition-all ${state.hours === option
|
||||||
? optionSelectedClass
|
? optionSelectedClass
|
||||||
: optionIdleClass
|
: optionIdleClass
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${state.hours === option ? 'bg-[var(--accent-soft)] text-[var(--accent)]' : 'bg-[var(--muted)] text-[var(--muted-foreground)]'}`}>
|
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${state.hours === option ? 'bg-[var(--accent-soft)] text-[var(--accent)]' : 'bg-[var(--muted)] text-[var(--muted-foreground)]'}`}>
|
||||||
<Clock size={20} />
|
<Clock size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="block text-lg font-bold text-[var(--foreground)]">{option}</span>
|
<span className="block text-lg font-bold text-[var(--foreground)]">{option}</span>
|
||||||
<span className="text-xs text-[var(--muted-foreground)]">
|
<span className="text-xs text-[var(--muted-foreground)]">
|
||||||
{option === '24/7' ? 'Universal coverage, trades any time signals appear.' :
|
{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).' :
|
option === 'London + New York' ? 'Focuses on the most liquid market overlap (07:00 - 21:00 UTC).' :
|
||||||
'Optimized for Tokoyo and Sydney sessions.'}
|
'Optimized for Tokoyo and Sydney sessions.'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-between pt-4">
|
||||||
<Button onClick={back} variant="ghost" className="px-6">
|
<Button onClick={back} variant="ghost" className="px-6">
|
||||||
<ChevronLeft size={18} /> Back
|
<ChevronLeft size={18} /> Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={next} className="px-8">
|
<Button onClick={next} className="px-8">
|
||||||
Continue <ChevronRight size={18} />
|
Continue <ChevronRight size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 4: Safety Confirmation */}
|
{/* Step 4: Safety Confirmation */}
|
||||||
{state.step === 4 && (
|
{state.step === 4 && (
|
||||||
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className={sectionTitleClass}>Safety Confirmation</h2>
|
<h2 className={sectionTitleClass}>Safety Confirmation</h2>
|
||||||
<p className={sectionDescriptionClass}>Review the built-in safeguards protecting your capital.</p>
|
<p className={sectionDescriptionClass}>Review the built-in safeguards protecting your capital.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="overflow-hidden rounded-2xl">
|
<Card className="overflow-hidden rounded-2xl">
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div className="flex items-center justify-between border-b border-[var(--border)] py-3">
|
<div className="flex items-center justify-between border-b border-[var(--border)] py-3">
|
||||||
<span className="text-sm font-medium text-[var(--muted-foreground)]">Auto-Protective Stop Loss</span>
|
<span className="text-sm font-medium text-[var(--muted-foreground)]">Auto-Protective Stop Loss</span>
|
||||||
<span className="text-sm font-bold text-[var(--accent)]">Enabled (Mandatory)</span>
|
<span className="text-sm font-bold text-[var(--accent)]">Enabled (Mandatory)</span>
|
||||||
@ -397,36 +395,36 @@ export const StrategyWizard: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between border-b border-[var(--border)] py-3">
|
<div className="flex items-center justify-between border-b border-[var(--border)] py-3">
|
||||||
<span className="text-sm font-medium text-[var(--muted-foreground)]">Daily Recovery Halt</span>
|
<span className="text-sm font-medium text-[var(--muted-foreground)]">Daily Recovery Halt</span>
|
||||||
<span className="text-rose-400 text-sm font-bold font-mono">-${Math.floor(state.capital * 0.05)} (5%)</span>
|
<span className="text-rose-400 text-sm font-bold font-mono">-${Math.floor(state.capital * 0.05)} (5%)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between py-3">
|
<div className="flex items-center justify-between py-3">
|
||||||
<span className="text-sm font-medium text-[var(--muted-foreground)]">Market Volatility Guard</span>
|
<span className="text-sm font-medium text-[var(--muted-foreground)]">Market Volatility Guard</span>
|
||||||
<span className="text-blue-400 text-sm font-bold">Smart ATR Check</span>
|
<span className="text-blue-400 text-sm font-bold">Smart ATR Check</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-amber-500/10 border-t border-amber-500/20 p-5 flex gap-4">
|
<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} />
|
<AlertTriangle className="text-amber-500 shrink-0" size={20} />
|
||||||
<p className="text-xs text-amber-500/90 leading-relaxed font-medium">
|
<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.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-between pt-4">
|
||||||
<Button onClick={back} variant="ghost" className="px-6">
|
<Button onClick={back} variant="ghost" className="px-6">
|
||||||
<ChevronLeft size={18} /> Back
|
<ChevronLeft size={18} /> Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={next} className="px-8">
|
<Button onClick={next} className="px-8">
|
||||||
I Understand <ChevronRight size={18} />
|
I Understand <ChevronRight size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 5: Review & Create */}
|
{/* Step 5: Review & Create */}
|
||||||
{state.step === 5 && (
|
{state.step === 5 && (
|
||||||
<div className="space-y-8 animate-in fade-in zoom-in-95 duration-500">
|
<div className="space-y-8 animate-in fade-in zoom-in-95 duration-500">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="mb-2 text-3xl font-bold text-[var(--foreground)]">Ready to Deploy</h2>
|
<h2 className="mb-2 text-3xl font-bold text-[var(--foreground)]">Ready to Deploy</h2>
|
||||||
<p className={sectionDescriptionClass}>Verify your bot strategy one last time.</p>
|
<p className={sectionDescriptionClass}>Verify your bot strategy one last time.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -444,41 +442,41 @@ export const StrategyWizard: React.FC<{
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-8 pt-4">
|
<div className="grid grid-cols-2 gap-8 pt-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Target Capital</span>
|
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Target Capital</span>
|
||||||
<div className="flex items-baseline gap-1 text-xl font-bold text-[var(--foreground)]">
|
<div className="flex items-baseline gap-1 text-xl font-bold text-[var(--foreground)]">
|
||||||
<span className="text-sm opacity-50 opacity-40">$</span>{state.capital}
|
<span className="text-sm opacity-50 opacity-40">$</span>{state.capital}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Profit Threshold</span>
|
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Profit Threshold</span>
|
||||||
<div className="flex items-baseline gap-1 text-xl font-bold text-[var(--accent)]">
|
<div className="flex items-baseline gap-1 text-xl font-bold text-[var(--accent)]">
|
||||||
<span className="text-sm opacity-40">$</span>{state.profitTarget}<span className="text-[10px] opacity-60 ml-1">/day</span>
|
<span className="text-sm opacity-40">$</span>{state.profitTarget}<span className="text-[10px] opacity-60 ml-1">/day</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Risk Level</span>
|
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Risk Level</span>
|
||||||
<div className="text-xl font-bold text-[var(--foreground)]">{(state.riskStyle?.riskPerTrade || 0)}% <span className="text-xs font-normal opacity-50">per trade</span></div>
|
<div className="text-xl font-bold text-[var(--foreground)]">{(state.riskStyle?.riskPerTrade || 0)}% <span className="text-xs font-normal opacity-50">per trade</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Active Schedule</span>
|
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Active Schedule</span>
|
||||||
<div className="text-xl font-bold text-[var(--foreground)]">{state.hours}</div>
|
<div className="text-xl font-bold text-[var(--foreground)]">{state.hours}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 border-t border-[var(--border)] pt-6">
|
<div className="flex items-center gap-4 border-t border-[var(--border)] pt-6">
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-[var(--border)] text-[var(--muted-foreground)]">
|
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-[var(--border)] text-[var(--muted-foreground)]">
|
||||||
<Wallet size={20} />
|
<Wallet size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm text-[var(--muted-foreground)]">Bots are created in <span className="font-bold text-[var(--foreground)]">PAUSED</span> mode by default. You must manually enable trading from your dashboard.</p>
|
<p className="text-sm text-[var(--muted-foreground)]">Bots are created in <span className="font-bold text-[var(--foreground)]">PAUSED</span> mode by default. You must manually enable trading from your dashboard.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div className="flex justify-between pt-4">
|
||||||
<Button onClick={back} variant="ghost" className="px-6">
|
<Button onClick={back} variant="ghost" className="px-6">
|
||||||
<ChevronLeft size={18} /> Back
|
<ChevronLeft size={18} /> Back
|
||||||
|
|||||||
@ -493,3 +493,36 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------------
|
||||||
|
Section 25 — card-button utility (UI audit Pattern A)
|
||||||
|
Use on a native <button> when you need a button-shaped CARD with stacked
|
||||||
|
block content. The @bytelyst/ui Button primitive has whitespace-nowrap +
|
||||||
|
fixed h-{size} that collapses multi-line content. This class restores the
|
||||||
|
focus ring + reset behavior without those constraints.
|
||||||
|
--------------------------------------------------------------------------- */
|
||||||
|
.card-button {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
|
height: auto;
|
||||||
|
transition: background-color 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
|
||||||
|
}
|
||||||
|
.card-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
.card-button:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--bl-bg-canvas, #0b0f17),
|
||||||
|
0 0 0 4px var(--bl-focus-ring, var(--bl-accent, #5A8CFF));
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|||||||
@ -5,21 +5,21 @@ import { getUserTier } from '../lib/TierPolicy';
|
|||||||
import {
|
import {
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
Trash2,
|
Trash2,
|
||||||
Activity,
|
Activity,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Plus,
|
Plus,
|
||||||
Shield,
|
Shield,
|
||||||
Zap,
|
Zap,
|
||||||
Scale,
|
Scale,
|
||||||
Settings,
|
Settings,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
Cpu,
|
Cpu,
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
Target,
|
Target,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Lock
|
Lock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { StrategyWizard } from '../components/StrategyWizard';
|
import { StrategyWizard } from '../components/StrategyWizard';
|
||||||
@ -35,8 +35,8 @@ function getStrategyKindLabel(config: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ActiveStrategyCard: React.FC<{
|
const ActiveStrategyCard: React.FC<{
|
||||||
profile: any;
|
profile: any;
|
||||||
botState: any;
|
botState: any;
|
||||||
tier: string;
|
tier: string;
|
||||||
onToggle: (p: any) => void;
|
onToggle: (p: any) => void;
|
||||||
onEdit: (p: any) => void;
|
onEdit: (p: any) => void;
|
||||||
@ -49,57 +49,57 @@ const ActiveStrategyCard: React.FC<{
|
|||||||
const isAggressive = config?.execution?.minRulePassRatio < 0.9;
|
const isAggressive = config?.execution?.minRulePassRatio < 0.9;
|
||||||
const isSafe = config?.execution?.minRulePassRatio >= 1.0;
|
const isSafe = config?.execution?.minRulePassRatio >= 1.0;
|
||||||
const strategyKindLabel = getStrategyKindLabel(config);
|
const strategyKindLabel = getStrategyKindLabel(config);
|
||||||
|
|
||||||
const explanation = getStrategyExplanation(profile, botState);
|
const explanation = getStrategyExplanation(profile, botState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'var(--card-elevated)',
|
background: 'var(--card-elevated)',
|
||||||
borderRadius: '28px',
|
borderRadius: '28px',
|
||||||
border: `1px solid ${profile.is_active ? 'var(--bl-success-muted)' : 'var(--bl-border-subtle)'}`,
|
border: `1px solid ${profile.is_active ? 'var(--bl-success-muted)' : 'var(--bl-border-subtle)'}`,
|
||||||
padding: '32px',
|
padding: '32px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
minHeight: '620px'
|
minHeight: '620px'
|
||||||
}} className="strategy-card-hover">
|
}} className="strategy-card-hover">
|
||||||
|
|
||||||
{/* 1. Direct Status Strip */}
|
{/* 1. Direct Status Strip */}
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: '4px',
|
width: '4px',
|
||||||
background: profile.is_active ? 'var(--bl-success)' : 'var(--bl-border-subtle)',
|
background: profile.is_active ? 'var(--bl-success)' : 'var(--bl-border-subtle)',
|
||||||
opacity: profile.is_active ? 1 : 0.3
|
opacity: profile.is_active ? 1 : 0.3
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
{/* 2. Header Area */}
|
{/* 2. Header Area */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
|
||||||
<div style={{ display: 'flex', gap: '14px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: '14px', alignItems: 'center' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '44px',
|
width: '44px',
|
||||||
height: '44px',
|
height: '44px',
|
||||||
borderRadius: '14px',
|
borderRadius: '14px',
|
||||||
background: 'var(--bl-surface-highlight)',
|
background: 'var(--bl-surface-highlight)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
border: '1px solid var(--bl-border-subtle)',
|
border: '1px solid var(--bl-border-subtle)',
|
||||||
color: isSafe ? 'var(--bl-info-strong)' : isAggressive ? 'var(--bl-danger)' : 'var(--bl-success)'
|
color: isSafe ? 'var(--bl-info-strong)' : isAggressive ? 'var(--bl-danger)' : 'var(--bl-success)'
|
||||||
}}>
|
}}>
|
||||||
{isSafe ? <Shield size={20} /> : isAggressive ? <Zap size={20} /> : <Scale size={20} />}
|
{isSafe ? <Shield size={20} /> : isAggressive ? <Zap size={20} /> : <Scale size={20} />}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||||||
<span style={{ fontSize: '10px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Active Strategy</span>
|
<span style={{ fontSize: '10px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Active Strategy</span>
|
||||||
<span style={{ fontSize: '13px', fontWeight: 800, color: 'white', textTransform: 'uppercase' }}>
|
<span style={{ fontSize: '13px', fontWeight: 800, color: 'white', textTransform: 'uppercase' }}>
|
||||||
{profile.is_active ? 'Running' : 'Paused'}
|
{profile.is_active ? 'Running' : 'Paused'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
{onBacktest && (
|
{onBacktest && (
|
||||||
@ -108,128 +108,128 @@ const ActiveStrategyCard: React.FC<{
|
|||||||
<IconButton type="button" label={`Edit ${profile.name}`} icon={<Settings size={18} />} onClick={() => onEdit(profile)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
<IconButton type="button" label={`Edit ${profile.name}`} icon={<Settings size={18} />} onClick={() => onEdit(profile)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
||||||
<IconButton type="button" label={`Delete ${profile.name}`} icon={<Trash2 size={18} />} onClick={() => onDelete(profile.id)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
<IconButton type="button" label={`Delete ${profile.name}`} icon={<Trash2 size={18} />} onClick={() => onDelete(profile.id)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. Identity */}
|
{/* 3. Identity */}
|
||||||
<div style={{ marginBottom: '24px' }}>
|
<div style={{ marginBottom: '24px' }}>
|
||||||
<h3 style={{ fontSize: '24px', fontWeight: 950, color: 'white', letterSpacing: '-0.02em' }}>{profile.name}</h3>
|
<h3 style={{ fontSize: '24px', fontWeight: 950, color: 'white', letterSpacing: '-0.02em' }}>{profile.name}</h3>
|
||||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '12px' }}>
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '12px' }}>
|
||||||
<span style={{ background: 'var(--bl-success-muted)', color: 'var(--bl-success)', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid var(--bl-success-muted)', textTransform: 'uppercase' }}>
|
<span style={{ background: 'var(--bl-success-muted)', color: 'var(--bl-success)', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid var(--bl-success-muted)', textTransform: 'uppercase' }}>
|
||||||
{profile.symbols}
|
{profile.symbols}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ background: 'var(--bl-surface-highlight)', color: 'var(--bl-text-quiet)', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid var(--bl-border-subtle)', textTransform: 'uppercase' }}>
|
<span style={{ background: 'var(--bl-surface-highlight)', color: 'var(--bl-text-quiet)', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid var(--bl-border-subtle)', textTransform: 'uppercase' }}>
|
||||||
{strategyKindLabel}
|
{strategyKindLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4. Operational DNA (Specs) */}
|
{/* 4. Operational DNA (Specs) */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '14px', marginBottom: '28px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '14px', marginBottom: '28px' }}>
|
||||||
{[
|
{[
|
||||||
{ label: 'Allocation', value: `$${profile.allocated_capital.toLocaleString()}`, icon: <DollarSign size={14} />, color: 'var(--bl-success)' },
|
{ label: 'Allocation', value: `$${profile.allocated_capital.toLocaleString()}`, icon: <DollarSign size={14} />, color: 'var(--bl-success)' },
|
||||||
{ label: 'PnL (Global)', value: '$0.00', icon: <TrendingUp size={14} />, color: 'var(--bl-info-strong)' },
|
{ label: 'PnL (Global)', value: '$0.00', icon: <TrendingUp size={14} />, color: 'var(--bl-info-strong)' },
|
||||||
{ label: 'Target', value: `$${config?.riskLimits?.dailyProfitTargetUsd || 0}`, icon: <Target size={14} />, color: 'var(--bl-warning)' },
|
{ label: 'Target', value: `$${config?.riskLimits?.dailyProfitTargetUsd || 0}`, icon: <Target size={14} />, color: 'var(--bl-warning)' },
|
||||||
{ label: 'Latency', value: '5ms', icon: <Cpu size={14} />, color: 'var(--bl-danger)' }
|
{ label: 'Latency', value: '5ms', icon: <Cpu size={14} />, color: 'var(--bl-danger)' }
|
||||||
].map((spec, i) => (
|
].map((spec, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} style={{
|
||||||
background: 'var(--bl-surface-overlay)',
|
background: 'var(--bl-surface-overlay)',
|
||||||
border: '1px solid var(--bl-border-subtle)',
|
border: '1px solid var(--bl-border-subtle)',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '6px'
|
gap: '6px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--bl-text-quiet)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--bl-text-quiet)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase' }}>
|
||||||
{spec.icon} {spec.label}
|
{spec.icon} {spec.label}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: 'white', fontWeight: 900, fontSize: '16px' }}>{spec.value}</div>
|
<div style={{ color: 'white', fontWeight: 900, fontSize: '16px' }}>{spec.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 5. Health Diagnostic (Education Layer) */}
|
{/* 5. Health Diagnostic (Education Layer) */}
|
||||||
<div style={{ marginBottom: '32px' }}>
|
<div style={{ marginBottom: '32px' }}>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggleExpand(profile.id)}
|
onClick={() => onToggleExpand(profile.id)}
|
||||||
variant="ghost"
|
className="card-button"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
background: 'var(--bl-surface-highlight)',
|
background: 'var(--bl-surface-highlight)',
|
||||||
border: '1px solid var(--bl-border-subtle)',
|
border: '1px solid var(--bl-border-subtle)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
textAlign: 'left'
|
textAlign: 'left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--bl-success)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--bl-success)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase' }}>
|
||||||
<Fingerprint size={14} /> Diagnostic Intelligence {tier === 'free' && <Lock size={12} style={{ marginLeft: '4px', opacity: 0.5 }} />}
|
<Fingerprint size={14} /> Diagnostic Intelligence {tier === 'free' && <Lock size={12} style={{ marginLeft: '4px', opacity: 0.5 }} />}
|
||||||
</div>
|
</div>
|
||||||
{isExpanded ? <ChevronUp size={14} color="var(--bl-text-quiet)" /> : <ChevronDown size={14} color="var(--bl-text-quiet)" />}
|
{isExpanded ? <ChevronUp size={14} color="var(--bl-text-quiet)" /> : <ChevronDown size={14} color="var(--bl-text-quiet)" />}
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '12px', color: 'var(--bl-text-secondary)', margin: 0, fontWeight: 500 }}>
|
<p style={{ fontSize: '12px', color: 'var(--bl-text-secondary)', margin: 0, fontWeight: 500 }}>
|
||||||
{explanation.reason}
|
{explanation.reason}
|
||||||
</p>
|
</p>
|
||||||
{isExpanded && explanation.recommendation && (
|
{isExpanded && explanation.recommendation && (
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
background: 'var(--bl-warning-muted)',
|
background: 'var(--bl-warning-muted)',
|
||||||
border: '1px solid var(--bl-warning-muted)',
|
border: '1px solid var(--bl-warning-muted)',
|
||||||
borderRadius: '12px'
|
borderRadius: '12px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--bl-warning)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', marginBottom: '4px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--bl-warning)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', marginBottom: '4px' }}>
|
||||||
<Lightbulb size={12} /> Optimization
|
<Lightbulb size={12} /> Optimization
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '11px', color: 'var(--bl-warning)', margin: 0, fontStyle: 'italic', fontWeight: 600 }}>{explanation.recommendation}</p>
|
<p style={{ fontSize: '11px', color: 'var(--bl-warning)', margin: 0, fontStyle: 'italic', fontWeight: 600 }}>{explanation.recommendation}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 6. Action */}
|
{/* 6. Action */}
|
||||||
<div style={{ marginTop: 'auto' }}>
|
<div style={{ marginTop: 'auto' }}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggle(profile)}
|
onClick={() => onToggle(profile)}
|
||||||
variant={profile.is_active ? 'outline' : 'primary'}
|
variant={profile.is_active ? 'outline' : 'primary'}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '56px',
|
height: '56px',
|
||||||
background: profile.is_active ? 'var(--bl-surface-highlight)' : 'var(--bl-success)',
|
background: profile.is_active ? 'var(--bl-surface-highlight)' : 'var(--bl-success)',
|
||||||
color: profile.is_active ? 'var(--bl-text-secondary)' : 'black',
|
color: profile.is_active ? 'var(--bl-text-secondary)' : 'black',
|
||||||
borderRadius: '18px',
|
borderRadius: '18px',
|
||||||
border: profile.is_active ? '1px solid var(--bl-border-subtle)' : 'none',
|
border: profile.is_active ? '1px solid var(--bl-border-subtle)' : 'none',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
boxShadow: profile.is_active ? 'none' : '0 12px 36px -12px color-mix(in oklab, var(--bl-success) 40%, transparent)',
|
boxShadow: profile.is_active ? 'none' : '0 12px 36px -12px color-mix(in oklab, var(--bl-success) 40%, transparent)',
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
letterSpacing: '1.5px'
|
letterSpacing: '1.5px'
|
||||||
}}
|
}}
|
||||||
className="action-btn-hover"
|
className="action-btn-hover"
|
||||||
>
|
>
|
||||||
{profile.is_active ? (
|
{profile.is_active ? (
|
||||||
<><Pause size={18} fill="currentColor" /> PAUSE TRADING</>
|
<><Pause size={18} fill="currentColor" /> PAUSE TRADING</>
|
||||||
) : (
|
) : (
|
||||||
<><Play size={18} fill="currentColor" /> START TRADING</>
|
<><Play size={18} fill="currentColor" /> START TRADING</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
.strategy-card-hover:hover {
|
.strategy-card-hover:hover {
|
||||||
border-color: var(--bl-success-muted) !important;
|
border-color: var(--bl-success-muted) !important;
|
||||||
transform: translateY(-8px);
|
transform: translateY(-8px);
|
||||||
box-shadow: 0 40px 80px -20px color-mix(in oklab, var(--background) 80%, black) !important;
|
box-shadow: 0 40px 80px -20px color-mix(in oklab, var(--background) 80%, black) !important;
|
||||||
@ -239,23 +239,23 @@ const ActiveStrategyCard: React.FC<{
|
|||||||
background: color-mix(in oklab, var(--card) 88%, var(--foreground)) !important;
|
background: color-mix(in oklab, var(--card) 88%, var(--foreground)) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
.action-btn-hover:hover {
|
.action-btn-hover:hover {
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewAsCustomer?: boolean }> = ({
|
export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewAsCustomer?: boolean }> = ({
|
||||||
botState,
|
botState,
|
||||||
alerts = [],
|
alerts = [],
|
||||||
previewAsCustomer = false
|
previewAsCustomer = false
|
||||||
}) => {
|
}) => {
|
||||||
const { user, profile: userProfile } = useAuth();
|
const { user, profile: userProfile } = useAuth();
|
||||||
const [profiles, setProfiles] = useState<any[]>([]);
|
const [profiles, setProfiles] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [showWizard, setShowWizard] = useState(false);
|
const [showWizard, setShowWizard] = useState(false);
|
||||||
const [editingProfile, setEditingProfile] = useState<any>(null);
|
const [editingProfile, setEditingProfile] = useState<any>(null);
|
||||||
const [expandedExplanations, setExpandedExplanations] = useState<Record<string, boolean>>({});
|
const [expandedExplanations, setExpandedExplanations] = useState<Record<string, boolean>>({});
|
||||||
@ -263,7 +263,7 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
|||||||
|
|
||||||
const tier = getUserTier(userProfile);
|
const tier = getUserTier(userProfile);
|
||||||
const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
|
const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
|
||||||
|
|
||||||
const fetchProfiles = async () => {
|
const fetchProfiles = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -271,13 +271,13 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
|||||||
setProfiles(data || []);
|
setProfiles(data || []);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProfiles();
|
fetchProfiles();
|
||||||
window.addEventListener('profiles-updated', fetchProfiles);
|
window.addEventListener('profiles-updated', fetchProfiles);
|
||||||
return () => window.removeEventListener('profiles-updated', fetchProfiles);
|
return () => window.removeEventListener('profiles-updated', fetchProfiles);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const toggleBot = async (profile: any) => {
|
const toggleBot = async (profile: any) => {
|
||||||
try {
|
try {
|
||||||
await setTradeProfileActive(profile.id, !profile.is_active);
|
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
|
// existing UI remains silent on delete failure
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showWizard) {
|
if (showWizard) {
|
||||||
return (
|
return (
|
||||||
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
<div style={{ marginBottom: '24px' }}>
|
<div style={{ marginBottom: '24px' }}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -312,7 +312,7 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
|||||||
>
|
>
|
||||||
← Back to My Strategies
|
← Back to My Strategies
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<StrategyWizard
|
<StrategyWizard
|
||||||
editingProfile={editingProfile}
|
editingProfile={editingProfile}
|
||||||
profile={userProfile}
|
profile={userProfile}
|
||||||
@ -321,12 +321,12 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
|||||||
setShowWizard(false);
|
setShowWizard(false);
|
||||||
setEditingProfile(null);
|
setEditingProfile(null);
|
||||||
fetchProfiles();
|
fetchProfiles();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="strategy-workspace">
|
<div className="strategy-workspace">
|
||||||
<div className="strategy-workspace-header">
|
<div className="strategy-workspace-header">
|
||||||
@ -353,67 +353,67 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */}
|
{/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const activeSymbols = [...new Set(profiles.flatMap(p => p.symbols?.split(',').map((s: string) => s.trim()) || []))];
|
const activeSymbols = [...new Set(profiles.flatMap(p => p.symbols?.split(',').map((s: string) => s.trim()) || []))];
|
||||||
const recentAlerts = [...alerts].reverse().slice(0, 5);
|
const recentAlerts = [...alerts].reverse().slice(0, 5);
|
||||||
const symbolVolatility = activeSymbols
|
const symbolVolatility = activeSymbols
|
||||||
.filter(s => botState?.symbols?.[s])
|
.filter(s => botState?.symbols?.[s])
|
||||||
.map(s => ({ symbol: s, change: botState.symbols[s].change24h || 0 }))
|
.map(s => ({ symbol: s, change: botState.symbols[s].change24h || 0 }))
|
||||||
.sort((a, b) => Math.abs(b.change) - Math.abs(a.change));
|
.sort((a, b) => Math.abs(b.change) - Math.abs(a.change));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '20px', marginBottom: '48px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '20px', marginBottom: '48px' }}>
|
||||||
{/* Recent Activity */}
|
{/* Recent Activity */}
|
||||||
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
||||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: 'var(--bl-success)', boxShadow: '0 0 8px var(--bl-success)' }} />
|
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: 'var(--bl-success)', boxShadow: '0 0 8px var(--bl-success)' }} />
|
||||||
<span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Recent Activity</span>
|
<span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Recent Activity</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
{recentAlerts.map((alert, i) => {
|
{recentAlerts.map((alert, i) => {
|
||||||
const mins = Math.floor((Date.now() - alert.timestamp) / 60000);
|
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`;
|
const timeAgo = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h ago`;
|
||||||
return (
|
return (
|
||||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: 'var(--bl-surface-overlay)', borderRadius: '10px' }}>
|
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: 'var(--bl-surface-overlay)', borderRadius: '10px' }}>
|
||||||
<Activity size={13} color="var(--bl-info)" />
|
<Activity size={13} color="var(--bl-info)" />
|
||||||
<span style={{ fontSize: '12px', color: 'var(--bl-text-quiet)', fontWeight: 700, minWidth: '60px' }}>{alert.symbol}</span>
|
<span style={{ fontSize: '12px', color: 'var(--bl-text-quiet)', fontWeight: 700, minWidth: '60px' }}>{alert.symbol}</span>
|
||||||
<span style={{ fontSize: '11px', color: 'var(--bl-text-quiet)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{alert.message}</span>
|
<span style={{ fontSize: '11px', color: 'var(--bl-text-quiet)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{alert.message}</span>
|
||||||
<span style={{ fontSize: '10px', color: 'var(--bl-text-tertiary)', fontWeight: 700, whiteSpace: 'nowrap' }}>{timeAgo}</span>
|
<span style={{ fontSize: '10px', color: 'var(--bl-text-tertiary)', fontWeight: 700, whiteSpace: 'nowrap' }}>{timeAgo}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{recentAlerts.length === 0 && <div style={{ textAlign: 'center', padding: '16px', color: 'var(--bl-text-tertiary)', fontSize: '12px', fontStyle: 'italic' }}>No activity yet...</div>}
|
{recentAlerts.length === 0 && <div style={{ textAlign: 'center', padding: '16px', color: 'var(--bl-text-tertiary)', fontSize: '12px', fontStyle: 'italic' }}>No activity yet...</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Symbol-Specific Volatility */}
|
{/* Symbol-Specific Volatility */}
|
||||||
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
||||||
<TrendingUp size={15} color="var(--bl-info)" />
|
<TrendingUp size={15} color="var(--bl-info)" />
|
||||||
<span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Your Markets (24h)</span>
|
<span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Your Markets (24h)</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
{symbolVolatility.map(({ symbol, change }) => (
|
{symbolVolatility.map(({ symbol, change }) => (
|
||||||
<div key={symbol} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bl-surface-overlay)', borderRadius: '12px' }}>
|
<div key={symbol} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bl-surface-overlay)', borderRadius: '12px' }}>
|
||||||
<span style={{ fontWeight: 800, fontSize: '13px', color: 'white' }}>{symbol}</span>
|
<span style={{ fontWeight: 800, fontSize: '13px', color: 'white' }}>{symbol}</span>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
<div style={{ width: '60px', height: '3px', background: 'var(--bl-border-subtle)', borderRadius: '99px', overflow: 'hidden' }}>
|
<div style={{ width: '60px', height: '3px', background: 'var(--bl-border-subtle)', borderRadius: '99px', overflow: 'hidden' }}>
|
||||||
<div style={{ width: `${Math.min(Math.abs(change) * 5, 60)}px`, height: '100%', background: change >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', borderRadius: '99px' }} />
|
<div style={{ width: `${Math.min(Math.abs(change) * 5, 60)}px`, height: '100%', background: change >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', borderRadius: '99px' }} />
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: '12px', fontWeight: 900, fontFamily: 'monospace', color: change >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', minWidth: '55px', textAlign: 'right' }}>
|
<span style={{ fontSize: '12px', fontWeight: 900, fontFamily: 'monospace', color: change >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', minWidth: '55px', textAlign: 'right' }}>
|
||||||
{change >= 0 ? '+' : ''}{change.toFixed(2)}%
|
{change >= 0 ? '+' : ''}{change.toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{symbolVolatility.length === 0 && <div style={{ textAlign: 'center', padding: '16px', color: 'var(--bl-text-tertiary)', fontSize: '12px', fontStyle: 'italic' }}>Deploy a strategy to see its market data</div>}
|
{symbolVolatility.length === 0 && <div style={{ textAlign: 'center', padding: '16px', color: 'var(--bl-text-tertiary)', fontSize: '12px', fontStyle: 'italic' }}>Deploy a strategy to see its market data</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))',
|
||||||
@ -449,20 +449,20 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
|||||||
onToggleExpand={(id) => setExpandedExplanations(prev => ({ ...prev, [id]: !prev[id] }))}
|
onToggleExpand={(id) => setExpandedExplanations(prev => ({ ...prev, [id]: !prev[id] }))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{profiles.length === 0 && !isLoading && (
|
{profiles.length === 0 && !isLoading && (
|
||||||
<div style={{
|
<div style={{
|
||||||
gridColumn: '1 / -1',
|
gridColumn: '1 / -1',
|
||||||
padding: '100px',
|
padding: '100px',
|
||||||
background: 'var(--bl-surface-highlight)',
|
background: 'var(--bl-surface-highlight)',
|
||||||
border: '2px dashed var(--bl-border-subtle)',
|
border: '2px dashed var(--bl-border-subtle)',
|
||||||
borderRadius: '40px',
|
borderRadius: '40px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<Activity size={60} color="var(--bl-text-quiet)" style={{ marginBottom: '24px' }} />
|
<Activity size={60} color="var(--bl-text-quiet)" style={{ marginBottom: '24px' }} />
|
||||||
<h3 style={{ color: 'var(--bl-text-quiet)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px' }}>No strategies yet</h3>
|
<h3 style={{ color: 'var(--bl-text-quiet)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px' }}>No strategies yet</h3>
|
||||||
<p style={{ color: 'var(--bl-text-quiet)', fontSize: '14px', marginTop: '12px', maxWidth: '300px' }}>Create your first strategy to start monitoring markets and testing automated execution.</p>
|
<p style={{ color: 'var(--bl-text-quiet)', fontSize: '14px', marginTop: '12px', maxWidth: '300px' }}>Create your first strategy to start monitoring markets and testing automated execution.</p>
|
||||||
@ -474,16 +474,16 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
|
|||||||
>
|
>
|
||||||
GET STARTED
|
GET STARTED
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1006,15 +1006,14 @@ export function SimpleView() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({ type: 'clear-feedback' });
|
dispatch({ type: 'clear-feedback' });
|
||||||
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
dispatch({ type: 'set-selected-holding-trade-id', value: null });
|
||||||
updateDraft('side', 'buy');
|
updateDraft('side', 'buy');
|
||||||
}}
|
}}
|
||||||
variant="ghost"
|
className={`card-button h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
|
||||||
className={`h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
|
|
||||||
draft.side === 'buy'
|
draft.side === 'buy'
|
||||||
? 'border-[var(--primary)] bg-[var(--accent-soft)]'
|
? 'border-[var(--primary)] bg-[var(--accent-soft)]'
|
||||||
: 'border-[var(--border)] bg-[var(--card-elevated)]'
|
: 'border-[var(--border)] bg-[var(--card-elevated)]'
|
||||||
@ -1024,8 +1023,8 @@ export function SimpleView() {
|
|||||||
<div className="text-sm font-bold text-[var(--foreground)]">New short-term buy plan</div>
|
<div className="text-sm font-bold text-[var(--foreground)]">New short-term buy plan</div>
|
||||||
<div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Arm a dip-buy trigger and let the app manage the profit exit after fill.</div>
|
<div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Arm a dip-buy trigger and let the app manage the profit exit after fill.</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({ type: 'clear-feedback' });
|
dispatch({ type: 'clear-feedback' });
|
||||||
@ -1035,8 +1034,7 @@ export function SimpleView() {
|
|||||||
updateDraft('side', 'sell');
|
updateDraft('side', 'sell');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="ghost"
|
className={`card-button h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
|
||||||
className={`h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
|
|
||||||
draft.side === 'sell'
|
draft.side === 'sell'
|
||||||
? 'border-[var(--primary)] bg-[var(--accent-soft)]'
|
? 'border-[var(--primary)] bg-[var(--accent-soft)]'
|
||||||
: 'border-[var(--border)] bg-[var(--card-elevated)]'
|
: 'border-[var(--border)] bg-[var(--card-elevated)]'
|
||||||
@ -1046,7 +1044,7 @@ export function SimpleView() {
|
|||||||
<div className="text-sm font-bold text-[var(--foreground)]">Manage an existing holding</div>
|
<div className="text-sm font-bold text-[var(--foreground)]">Manage an existing holding</div>
|
||||||
<div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Choose a filled holding and place it back under managed profit-taking.</div>
|
<div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Choose a filled holding and place it back under managed profit-taking.</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user