224 lines
9.9 KiB
TypeScript
224 lines
9.9 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { fetchMarketplacePresets } from '../lib/marketplaceApi';
|
|
import {
|
|
Activity,
|
|
ArrowUpRight,
|
|
CheckCircle,
|
|
Cpu,
|
|
Fingerprint,
|
|
Info,
|
|
LineChart,
|
|
Scale,
|
|
Shield,
|
|
TrendingUp,
|
|
Users,
|
|
Zap,
|
|
} from 'lucide-react';
|
|
import type { StrategyPreset } from '../lib/PresetRegistry';
|
|
import { STRATEGY_PRESETS } from '../lib/PresetRegistry';
|
|
import { Button } from './ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
|
import { PageHeader } from './ui/page-header';
|
|
|
|
interface PresetMarketplaceProps {
|
|
onSelect: (preset: StrategyPreset) => void;
|
|
onClose?: () => void;
|
|
}
|
|
|
|
const themeByRisk: Record<string, { tone: 'safe' | 'balanced' | 'aggressive'; label: string }> = {
|
|
safe: { tone: 'safe', label: 'Low Volatility' },
|
|
balanced: { tone: 'balanced', label: 'Balanced' },
|
|
aggressive: { tone: 'aggressive', label: 'High Conviction' },
|
|
};
|
|
|
|
function metricForPreset(preset: StrategyPreset) {
|
|
if (preset.riskStyleId === 'aggressive') {
|
|
return { performance: '+14.2%', volatility: 'High', icon: Zap, accent: 'var(--destructive)' };
|
|
}
|
|
if (preset.riskStyleId === 'safe') {
|
|
return { performance: '+4.8%', volatility: 'Low', icon: Shield, accent: 'var(--primary)' };
|
|
}
|
|
return { performance: '+8.5%', volatility: 'Medium', icon: Scale, accent: 'var(--ring)' };
|
|
}
|
|
|
|
const StrategyMarketplaceCard: React.FC<{
|
|
preset: StrategyPreset;
|
|
onSelect: (preset: StrategyPreset) => void;
|
|
index: number;
|
|
}> = ({ preset, onSelect, index }) => {
|
|
const theme = themeByRisk[preset.riskStyleId] || themeByRisk.balanced;
|
|
const metric = metricForPreset(preset);
|
|
const RiskIcon = metric.icon;
|
|
|
|
return (
|
|
<Card className="h-full rounded-[28px] transition duration-200 hover:-translate-y-1 hover:border-[var(--ring)]/30 hover:shadow-xl">
|
|
<CardHeader className="gap-5">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className="flex h-11 w-11 items-center justify-center rounded-2xl border"
|
|
style={{
|
|
background: 'var(--accent-soft)',
|
|
borderColor: 'var(--border)',
|
|
color: metric.accent,
|
|
}}
|
|
>
|
|
<RiskIcon size={18} />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<div className="text-[10px] font-semibold uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
|
|
Strategy Profile
|
|
</div>
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-[var(--foreground)]">
|
|
{preset.riskStyleId} strategy
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="stat-chip">V{index + 1}.4</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<CardTitle className="text-2xl">{preset.name}</CardTitle>
|
|
<div
|
|
className="inline-flex w-fit items-center gap-2 rounded-lg border px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide"
|
|
style={{
|
|
background: 'var(--accent-soft)',
|
|
borderColor: 'var(--border)',
|
|
color: metric.accent,
|
|
}}
|
|
>
|
|
<Fingerprint size={13} />
|
|
{theme.label} • {metric.performance}
|
|
</div>
|
|
<CardDescription className="text-sm leading-6">
|
|
{preset.description} Optimized for automated execution without changing your core risk budget.
|
|
</CardDescription>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="flex h-full flex-col gap-6">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{[
|
|
{ label: 'Growth', value: metric.performance, icon: <TrendingUp size={14} /> },
|
|
{ label: 'Latency', value: 'Low', icon: <Cpu size={14} /> },
|
|
{ label: 'Liquidity', value: 'Prime', icon: <Users size={14} /> },
|
|
{ label: 'Volatility', value: metric.volatility, icon: <Activity size={14} /> },
|
|
].map((spec) => (
|
|
<div
|
|
key={spec.label}
|
|
className="rounded-2xl border p-4"
|
|
style={{ background: 'var(--card-elevated)', borderColor: 'var(--border)' }}
|
|
>
|
|
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-[var(--muted-foreground)]">
|
|
{spec.icon}
|
|
{spec.label}
|
|
</div>
|
|
<div className="text-base font-semibold text-[var(--foreground)]">{spec.value}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="space-y-2 text-sm text-[var(--muted-foreground)]">
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle size={15} className="text-[var(--primary)]" />
|
|
Logical invariant verified
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle size={15} className="text-[var(--primary)]" />
|
|
Risk-isolated execution
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-auto flex gap-3">
|
|
<Button className="h-12 flex-1 rounded-2xl" onClick={() => onSelect(preset)}>
|
|
Use This Strategy
|
|
<ArrowUpRight size={15} />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-12 w-12 rounded-2xl"
|
|
title="Preset information"
|
|
>
|
|
<Info size={18} />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export const PresetMarketplace: React.FC<PresetMarketplaceProps> = ({ onSelect, onClose }) => {
|
|
const [customPresets, setCustomPresets] = useState<StrategyPreset[]>([]);
|
|
|
|
useEffect(() => {
|
|
const fetchCustomPresets = async () => {
|
|
try {
|
|
const data = await fetchMarketplacePresets();
|
|
const mappedData = data.map((d: any) => ({
|
|
id: d.id,
|
|
name: d.name,
|
|
description: d.description,
|
|
riskStyleId: d.risk_style_id,
|
|
recommendedAssets: d.recommended_assets,
|
|
typicalTradesPerDay: d.typical_trades_per_day,
|
|
performanceTag: d.performance_tag,
|
|
isPopular: d.is_popular,
|
|
strategy_config: d.strategy_config,
|
|
}));
|
|
setCustomPresets(mappedData as StrategyPreset[]);
|
|
} catch (e) {
|
|
console.error('Error fetching marketplace presets:', e);
|
|
}
|
|
};
|
|
|
|
void fetchCustomPresets();
|
|
}, []);
|
|
|
|
const allPresets = useMemo(() => [...STRATEGY_PRESETS, ...customPresets], [customPresets]);
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
<PageHeader
|
|
title="Strategy Marketplace"
|
|
description="Browse reusable strategy profiles with preconfigured risk posture and execution bias."
|
|
/>
|
|
<div className="flex items-center gap-3">
|
|
<div className="stat-chip">{allPresets.length} presets</div>
|
|
{onClose ? (
|
|
<Button variant="outline" onClick={onClose}>
|
|
Return
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
|
{allPresets.map((preset, idx) => (
|
|
<StrategyMarketplaceCard key={preset.id} preset={preset} index={idx} onSelect={onSelect} />
|
|
))}
|
|
|
|
<Card className="rounded-[28px] border-dashed">
|
|
<CardContent className="flex min-h-[560px] flex-col items-center justify-center gap-4 px-8 py-10 text-center">
|
|
<div
|
|
className="flex h-16 w-16 items-center justify-center rounded-3xl border"
|
|
style={{ background: 'var(--accent-soft)', borderColor: 'var(--border)' }}
|
|
>
|
|
<LineChart size={28} className="text-[var(--muted-foreground)]" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
|
|
Analyzing DNA
|
|
</div>
|
|
<p className="mx-auto max-w-xs text-sm text-[var(--muted-foreground)]">
|
|
Verification queue is active for new marketplace strategies.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|