initial charts / backtest
This commit is contained in:
parent
11c24b2280
commit
1b9010ebf4
37 changed files with 3888 additions and 23 deletions
|
|
@ -12,13 +12,19 @@
|
|||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.3.1",
|
||||
"lightweight-charts": "^4.1.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"react-virtuoso": "^4.12.8",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"recharts": "^2.10.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { DashboardPage } from '@/features/dashboard';
|
|||
import { ExchangesPage } from '@/features/exchanges';
|
||||
import { MonitoringPage } from '@/features/monitoring';
|
||||
import { PipelinePage } from '@/features/pipeline';
|
||||
import { BacktestPage } from '@/features/backtest';
|
||||
import { ChartPage } from '@/features/charts';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
export function App() {
|
||||
|
|
@ -12,6 +14,7 @@ export function App() {
|
|||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<DashboardPage />} />
|
||||
<Route path="charts" element={<ChartPage />} />
|
||||
<Route path="exchanges" element={<ExchangesPage />} />
|
||||
<Route
|
||||
path="portfolio"
|
||||
|
|
@ -25,6 +28,7 @@ export function App() {
|
|||
path="analytics"
|
||||
element={<div className="p-4">Analytics Page - Coming Soon</div>}
|
||||
/>
|
||||
<Route path="backtest" element={<BacktestPage />} />
|
||||
<Route path="settings" element={<div className="p-4">Settings Page - Coming Soon</div>} />
|
||||
<Route path="system/monitoring" element={<MonitoringPage />} />
|
||||
<Route path="system/pipeline" element={<PipelinePage />} />
|
||||
|
|
|
|||
69
apps/stock/web-app/src/features/backtest/BacktestPage.tsx
Normal file
69
apps/stock/web-app/src/features/backtest/BacktestPage.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { BacktestConfiguration } from './components/BacktestConfiguration';
|
||||
import { BacktestResults } from './components/BacktestResults';
|
||||
import { BacktestControls } from './components/BacktestControls';
|
||||
import { useBacktest } from './hooks/useBacktest';
|
||||
|
||||
export function BacktestPage() {
|
||||
const {
|
||||
config,
|
||||
status,
|
||||
results,
|
||||
currentTime,
|
||||
error,
|
||||
isLoading,
|
||||
handleConfigSubmit,
|
||||
handleStart,
|
||||
handlePause,
|
||||
handleResume,
|
||||
handleStop,
|
||||
handleStep,
|
||||
} = useBacktest();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full space-y-6">
|
||||
<div className="flex-shrink-0">
|
||||
<h1 className="text-lg font-bold text-text-primary mb-2">Backtest Strategy</h1>
|
||||
<p className="text-text-secondary mb-6 text-sm">
|
||||
Test your trading strategies against historical data to evaluate performance and risk.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-error/10 border border-error text-error px-4 py-3 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 flex-1 min-h-0">
|
||||
<div className="lg:col-span-1 space-y-4">
|
||||
<BacktestConfiguration
|
||||
onSubmit={handleConfigSubmit}
|
||||
disabled={status === 'running' || isLoading}
|
||||
/>
|
||||
|
||||
{config && (
|
||||
<BacktestControls
|
||||
status={status}
|
||||
onStart={handleStart}
|
||||
onPause={handlePause}
|
||||
onResume={handleResume}
|
||||
onStop={handleStop}
|
||||
onStep={handleStep}
|
||||
currentTime={currentTime}
|
||||
startTime={config.startDate.getTime()}
|
||||
endTime={config.endDate.getTime()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<BacktestResults
|
||||
status={status}
|
||||
results={results}
|
||||
currentTime={currentTime}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
import { useState } from 'react';
|
||||
import type { BacktestConfig } from '../types';
|
||||
|
||||
interface BacktestConfigurationProps {
|
||||
onSubmit: (config: BacktestConfig) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurationProps) {
|
||||
const [formData, setFormData] = useState<BacktestConfig>({
|
||||
name: '',
|
||||
startDate: new Date(new Date().setMonth(new Date().getMonth() - 6)), // 6 months ago
|
||||
endDate: new Date(),
|
||||
initialCapital: 100000,
|
||||
symbols: [],
|
||||
strategy: 'momentum',
|
||||
speedMultiplier: 1,
|
||||
commission: 0.001, // 0.1%
|
||||
slippage: 0.0005, // 0.05%
|
||||
});
|
||||
|
||||
const [symbolInput, setSymbolInput] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (formData.symbols.length === 0) {
|
||||
alert('Please add at least one symbol');
|
||||
return;
|
||||
}
|
||||
onSubmit(formData);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: type === 'number' ? parseFloat(value) :
|
||||
type === 'date' ? new Date(value) : value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddSymbol = () => {
|
||||
const symbol = symbolInput.trim().toUpperCase();
|
||||
if (symbol && !formData.symbols.includes(symbol)) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
symbols: [...prev.symbols, symbol]
|
||||
}));
|
||||
setSymbolInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSymbol = (symbol: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
symbols: prev.symbols.filter(s => s !== symbol)
|
||||
}));
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h2 className="text-base font-medium text-text-primary mb-4">Configuration</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Backtest Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="My Strategy Backtest"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="startDate"
|
||||
value={formatDate(formData.startDate)}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="endDate"
|
||||
value={formatDate(formData.endDate)}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Initial Capital ($)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="initialCapital"
|
||||
value={formData.initialCapital}
|
||||
onChange={handleChange}
|
||||
min="1000"
|
||||
step="1000"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Symbols
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={symbolInput}
|
||||
onChange={(e) => setSymbolInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddSymbol())}
|
||||
placeholder="AAPL"
|
||||
className="flex-1 px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddSymbol}
|
||||
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors disabled:opacity-50"
|
||||
disabled={disabled}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.symbols.map(symbol => (
|
||||
<span
|
||||
key={symbol}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-primary-500/10 text-primary-400 rounded-md text-sm"
|
||||
>
|
||||
{symbol}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveSymbol(symbol)}
|
||||
className="text-primary-400 hover:text-primary-300"
|
||||
disabled={disabled}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Strategy
|
||||
</label>
|
||||
<select
|
||||
name="strategy"
|
||||
value={formData.strategy}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
>
|
||||
<option value="momentum">Momentum</option>
|
||||
<option value="mean-reversion">Mean Reversion</option>
|
||||
<option value="pairs-trading">Pairs Trading</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Commission (%)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="commission"
|
||||
value={formData.commission * 100}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, commission: parseFloat(e.target.value) / 100 }))}
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Slippage (%)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="slippage"
|
||||
value={formData.slippage * 100}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, slippage: parseFloat(e.target.value) / 100 }))}
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1">
|
||||
Speed Multiplier
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="speedMultiplier"
|
||||
value={formData.speedMultiplier}
|
||||
onChange={handleChange}
|
||||
min="0.1"
|
||||
max="1000"
|
||||
step="0.1"
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<p className="text-xs text-text-muted mt-1">1x = real-time, 10x = 10x faster</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={disabled}
|
||||
>
|
||||
Configure Backtest
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import {
|
||||
ArrowPathIcon,
|
||||
ForwardIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
StopIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import type { BacktestStatus } from '../types';
|
||||
|
||||
interface BacktestControlsProps {
|
||||
status: BacktestStatus;
|
||||
onStart: () => void;
|
||||
onPause: () => void;
|
||||
onResume: () => void;
|
||||
onStop: () => void;
|
||||
onStep: () => void;
|
||||
currentTime: number | null;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export function BacktestControls({
|
||||
status,
|
||||
onStart,
|
||||
onPause,
|
||||
onResume,
|
||||
onStop,
|
||||
onStep,
|
||||
currentTime,
|
||||
startTime,
|
||||
endTime,
|
||||
}: BacktestControlsProps) {
|
||||
const progress = currentTime
|
||||
? ((currentTime - startTime) / (endTime - startTime)) * 100
|
||||
: 0;
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h2 className="text-base font-medium text-text-primary mb-4">Controls</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
{status === 'configured' || status === 'stopped' ? (
|
||||
<button
|
||||
onClick={onStart}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-success text-white rounded-md text-sm font-medium hover:bg-success/90 transition-colors"
|
||||
>
|
||||
<PlayIcon className="w-4 h-4" />
|
||||
Start
|
||||
</button>
|
||||
) : status === 'running' ? (
|
||||
<button
|
||||
onClick={onPause}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-warning text-white rounded-md text-sm font-medium hover:bg-warning/90 transition-colors"
|
||||
>
|
||||
<PauseIcon className="w-4 h-4" />
|
||||
Pause
|
||||
</button>
|
||||
) : status === 'paused' ? (
|
||||
<button
|
||||
onClick={onResume}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-success text-white rounded-md text-sm font-medium hover:bg-success/90 transition-colors"
|
||||
>
|
||||
<PlayIcon className="w-4 h-4" />
|
||||
Resume
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{(status === 'running' || status === 'paused') && (
|
||||
<button
|
||||
onClick={onStop}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-error text-white rounded-md text-sm font-medium hover:bg-error/90 transition-colors"
|
||||
>
|
||||
<StopIcon className="w-4 h-4" />
|
||||
Stop
|
||||
</button>
|
||||
)}
|
||||
|
||||
{status === 'paused' && (
|
||||
<button
|
||||
onClick={onStep}
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
>
|
||||
<ForwardIcon className="w-4 h-4" />
|
||||
Step
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status !== 'idle' && status !== 'configured' && (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm text-text-secondary mb-1">
|
||||
<span>Progress</span>
|
||||
<span>{progress.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-background rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="bg-primary-500 h-2 transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Status</span>
|
||||
<span className={`text-text-primary font-medium ${
|
||||
status === 'running' ? 'text-success' :
|
||||
status === 'paused' ? 'text-warning' :
|
||||
status === 'completed' ? 'text-primary-400' :
|
||||
status === 'error' ? 'text-error' :
|
||||
'text-text-muted'
|
||||
}`}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{currentTime && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Current Time</span>
|
||||
<span className="text-text-primary text-xs">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'completed' && (
|
||||
<button
|
||||
onClick={onStart}
|
||||
className="w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
>
|
||||
<ArrowPathIcon className="w-4 h-4" />
|
||||
Run Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import type { BacktestResult, BacktestStatus } from '../types';
|
||||
import { MetricsCard } from './MetricsCard';
|
||||
import { PositionsTable } from './PositionsTable';
|
||||
import { TradeLog } from './TradeLog';
|
||||
|
||||
interface BacktestResultsProps {
|
||||
status: BacktestStatus;
|
||||
results: BacktestResult | null;
|
||||
currentTime: number | null;
|
||||
}
|
||||
|
||||
export function BacktestResults({ status, results, currentTime }: BacktestResultsProps) {
|
||||
if (status === 'idle') {
|
||||
return (
|
||||
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium text-text-primary mb-2">
|
||||
Configure Your Backtest
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">
|
||||
Set up your strategy parameters and click "Configure Backtest" to begin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'configured') {
|
||||
return (
|
||||
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium text-text-primary mb-2">
|
||||
Ready to Start
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">
|
||||
Click the "Start" button to begin backtesting your strategy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'running' && !results) {
|
||||
return (
|
||||
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-primary-500 mx-auto mb-4"></div>
|
||||
<h3 className="text-lg font-medium text-text-primary mb-2">
|
||||
Running Backtest...
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">
|
||||
Processing historical data and executing trades.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
return (
|
||||
<div className="bg-surface-secondary p-8 rounded-lg border border-border h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium text-text-primary mb-2">
|
||||
No Results Yet
|
||||
</h3>
|
||||
<p className="text-sm text-text-secondary">
|
||||
Results will appear here once the backtest is complete.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 h-full overflow-y-auto">
|
||||
{/* Metrics Overview */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<MetricsCard
|
||||
title="Total Return"
|
||||
value={`${results.metrics.totalReturn >= 0 ? '+' : ''}${results.metrics.totalReturn.toFixed(2)}%`}
|
||||
trend={results.metrics.totalReturn >= 0 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Sharpe Ratio"
|
||||
value={results.metrics.sharpeRatio.toFixed(2)}
|
||||
trend={results.metrics.sharpeRatio >= 1 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Max Drawdown"
|
||||
value={`${results.metrics.maxDrawdown.toFixed(2)}%`}
|
||||
trend="down"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Win Rate"
|
||||
value={`${results.metrics.winRate.toFixed(1)}%`}
|
||||
trend={results.metrics.winRate >= 50 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Total Trades"
|
||||
value={results.metrics.totalTrades.toString()}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Profitable Trades"
|
||||
value={results.metrics.profitableTrades.toString()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Performance Chart Placeholder */}
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h3 className="text-base font-medium text-text-primary mb-4">
|
||||
Portfolio Performance
|
||||
</h3>
|
||||
<div className="h-64 bg-background rounded border border-border flex items-center justify-center">
|
||||
<p className="text-sm text-text-muted">
|
||||
Performance chart will be displayed here (requires recharts)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Positions Table */}
|
||||
{results.positions.length > 0 && (
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h3 className="text-base font-medium text-text-primary mb-4">
|
||||
Current Positions
|
||||
</h3>
|
||||
<PositionsTable positions={results.positions} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Trade Log */}
|
||||
{results.trades.length > 0 && (
|
||||
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
|
||||
<h3 className="text-base font-medium text-text-primary mb-4">
|
||||
Trade History
|
||||
</h3>
|
||||
<TradeLog trades={results.trades} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/24/solid';
|
||||
|
||||
interface MetricsCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
trend?: 'up' | 'down';
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export function MetricsCard({ title, value, trend, subtitle }: MetricsCardProps) {
|
||||
return (
|
||||
<div className="bg-background p-4 rounded-lg border border-border">
|
||||
<h4 className="text-sm font-medium text-text-secondary mb-1">{title}</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className={`text-2xl font-bold ${
|
||||
trend === 'up' ? 'text-success' :
|
||||
trend === 'down' ? 'text-error' :
|
||||
'text-text-primary'
|
||||
}`}>
|
||||
{value}
|
||||
</p>
|
||||
{trend && (
|
||||
<span className={`inline-flex ${
|
||||
trend === 'up' ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{trend === 'up' ?
|
||||
<ArrowUpIcon className="w-4 h-4" /> :
|
||||
<ArrowDownIcon className="w-4 h-4" />
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-text-muted mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import type { Position } from '../types';
|
||||
|
||||
interface PositionsTableProps {
|
||||
positions: Position[];
|
||||
}
|
||||
|
||||
export function PositionsTable({ positions }: PositionsTableProps) {
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatPnl = (value: number) => {
|
||||
const formatted = formatCurrency(Math.abs(value));
|
||||
return value >= 0 ? `+${formatted}` : `-${formatted}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left py-2 px-2 font-medium text-text-secondary">Symbol</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Quantity</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Avg Price</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Current</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">P&L</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Unrealized</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{positions.map((position) => {
|
||||
const totalPnl = position.realizedPnl + position.unrealizedPnl;
|
||||
return (
|
||||
<tr key={position.symbol} className="border-b border-border hover:bg-surface-tertiary">
|
||||
<td className="py-2 px-2 font-medium text-text-primary">{position.symbol}</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{position.quantity.toLocaleString()}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{formatCurrency(position.averagePrice)}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{formatCurrency(position.currentPrice)}
|
||||
</td>
|
||||
<td className={`text-right py-2 px-2 font-medium ${
|
||||
totalPnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{formatPnl(totalPnl)}
|
||||
</td>
|
||||
<td className={`text-right py-2 px-2 ${
|
||||
position.unrealizedPnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{formatPnl(position.unrealizedPnl)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import type { Trade } from '../types';
|
||||
|
||||
interface TradeLogProps {
|
||||
trades: Trade[];
|
||||
}
|
||||
|
||||
export function TradeLog({ trades }: TradeLogProps) {
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
// Show latest trades first
|
||||
const sortedTrades = [...trades].reverse();
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto max-h-96">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-surface-secondary">
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left py-2 px-2 font-medium text-text-secondary">Time</th>
|
||||
<th className="text-left py-2 px-2 font-medium text-text-secondary">Symbol</th>
|
||||
<th className="text-center py-2 px-2 font-medium text-text-secondary">Side</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Quantity</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Price</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Value</th>
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">Comm.</th>
|
||||
{trades.some(t => t.pnl !== undefined) && (
|
||||
<th className="text-right py-2 px-2 font-medium text-text-secondary">P&L</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTrades.map((trade) => {
|
||||
const tradeValue = trade.quantity * trade.price;
|
||||
return (
|
||||
<tr key={trade.id} className="border-b border-border hover:bg-surface-tertiary">
|
||||
<td className="py-2 px-2 text-text-muted text-xs">
|
||||
{formatTime(trade.timestamp)}
|
||||
</td>
|
||||
<td className="py-2 px-2 font-medium text-text-primary">{trade.symbol}</td>
|
||||
<td className="text-center py-2 px-2">
|
||||
<span className={`inline-flex px-2 py-0.5 text-xs font-medium rounded ${
|
||||
trade.side === 'buy'
|
||||
? 'bg-success/10 text-success'
|
||||
: 'bg-error/10 text-error'
|
||||
}`}>
|
||||
{trade.side.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{trade.quantity.toLocaleString()}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{formatCurrency(trade.price)}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-primary">
|
||||
{formatCurrency(tradeValue)}
|
||||
</td>
|
||||
<td className="text-right py-2 px-2 text-text-muted">
|
||||
{formatCurrency(trade.commission)}
|
||||
</td>
|
||||
{trade.pnl !== undefined && (
|
||||
<td className={`text-right py-2 px-2 font-medium ${
|
||||
trade.pnl >= 0 ? 'text-success' : 'text-error'
|
||||
}`}>
|
||||
{trade.pnl >= 0 ? '+' : ''}{formatCurrency(trade.pnl)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export { BacktestConfiguration } from './BacktestConfiguration';
|
||||
export { BacktestControls } from './BacktestControls';
|
||||
export { BacktestResults } from './BacktestResults';
|
||||
export { MetricsCard } from './MetricsCard';
|
||||
export { PositionsTable } from './PositionsTable';
|
||||
export { TradeLog } from './TradeLog';
|
||||
1
apps/stock/web-app/src/features/backtest/hooks/index.ts
Normal file
1
apps/stock/web-app/src/features/backtest/hooks/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useBacktest } from './useBacktest';
|
||||
169
apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts
Normal file
169
apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { BacktestService } from '../services/backtestService';
|
||||
import type { BacktestConfig, BacktestResult, BacktestStatus } from '../types';
|
||||
|
||||
export function useBacktest() {
|
||||
const [backtestId, setBacktestId] = useState<string | null>(null);
|
||||
const [config, setConfig] = useState<BacktestConfig | null>(null);
|
||||
const [status, setStatus] = useState<BacktestStatus>('idle');
|
||||
const [results, setResults] = useState<BacktestResult | null>(null);
|
||||
const [currentTime, setCurrentTime] = useState<number | null>(null);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const handleConfigSubmit = useCallback(async (newConfig: BacktestConfig) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Create backtest
|
||||
const { id } = await BacktestService.createBacktest(newConfig);
|
||||
|
||||
setBacktestId(id);
|
||||
setConfig(newConfig);
|
||||
setStatus('configured');
|
||||
setResults(null);
|
||||
setCurrentTime(null);
|
||||
setProgress(0);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleStart = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.startBacktest(backtestId);
|
||||
setStatus('running');
|
||||
|
||||
// Start polling for updates
|
||||
cleanupRef.current = await BacktestService.pollBacktestUpdates(
|
||||
backtestId,
|
||||
(newStatus, newProgress, newTime) => {
|
||||
setStatus(newStatus);
|
||||
if (newProgress !== undefined) setProgress(newProgress);
|
||||
if (newTime !== undefined) setCurrentTime(newTime);
|
||||
|
||||
// Fetch full results when completed
|
||||
if (newStatus === 'completed') {
|
||||
BacktestService.getBacktestResults(backtestId).then(setResults);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to start backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
const handlePause = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.pauseBacktest(backtestId);
|
||||
setStatus('paused');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to pause backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
const handleResume = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.resumeBacktest(backtestId);
|
||||
setStatus('running');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to resume backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.stopBacktest(backtestId);
|
||||
setStatus('stopped');
|
||||
|
||||
// Stop polling
|
||||
if (cleanupRef.current) {
|
||||
cleanupRef.current();
|
||||
cleanupRef.current = null;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to stop backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
const handleStep = useCallback(async () => {
|
||||
if (!backtestId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
await BacktestService.stepBacktest(backtestId);
|
||||
|
||||
// Get updated status
|
||||
const statusUpdate = await BacktestService.getBacktestStatus(backtestId);
|
||||
setStatus(statusUpdate.status);
|
||||
if (statusUpdate.progress !== undefined) setProgress(statusUpdate.progress);
|
||||
if (statusUpdate.currentTime !== undefined) setCurrentTime(statusUpdate.currentTime);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to step backtest');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [backtestId]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (cleanupRef.current) {
|
||||
cleanupRef.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
backtestId,
|
||||
config,
|
||||
status,
|
||||
results,
|
||||
currentTime,
|
||||
progress,
|
||||
error,
|
||||
isLoading,
|
||||
handleConfigSubmit,
|
||||
handleStart,
|
||||
handlePause,
|
||||
handleResume,
|
||||
handleStop,
|
||||
handleStep,
|
||||
};
|
||||
}
|
||||
2
apps/stock/web-app/src/features/backtest/index.ts
Normal file
2
apps/stock/web-app/src/features/backtest/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { BacktestPage } from './BacktestPage';
|
||||
export * from './types';
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import type { BacktestConfig, BacktestResult, BacktestStatus } from '../types';
|
||||
|
||||
const API_BASE = '/api/backtest';
|
||||
|
||||
export class BacktestService {
|
||||
static async createBacktest(config: BacktestConfig): Promise<{ id: string }> {
|
||||
const response = await fetch(`${API_BASE}/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...config,
|
||||
startDate: config.startDate.toISOString(),
|
||||
endDate: config.endDate.toISOString(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create backtest');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async startBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/start`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to start backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async pauseBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/pause`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to pause backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async resumeBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/resume`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to resume backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async stopBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/stop`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to stop backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async stepBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}/step`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to step backtest');
|
||||
}
|
||||
}
|
||||
|
||||
static async getBacktestStatus(id: string): Promise<{
|
||||
status: BacktestStatus;
|
||||
currentTime?: number;
|
||||
progress?: number;
|
||||
}> {
|
||||
const response = await fetch(`${API_BASE}/${id}/status`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get backtest status');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async getBacktestResults(id: string): Promise<BacktestResult> {
|
||||
const response = await fetch(`${API_BASE}/${id}/results`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get backtest results');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Convert date strings back to Date objects
|
||||
return {
|
||||
...data,
|
||||
config: {
|
||||
...data.config,
|
||||
startDate: new Date(data.config.startDate),
|
||||
endDate: new Date(data.config.endDate),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static async listBacktests(): Promise<BacktestResult[]> {
|
||||
const response = await fetch(`${API_BASE}/list`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to list backtests');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Convert date strings back to Date objects
|
||||
return data.map((backtest: any) => ({
|
||||
...backtest,
|
||||
config: {
|
||||
...backtest.config,
|
||||
startDate: new Date(backtest.config.startDate),
|
||||
endDate: new Date(backtest.config.endDate),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
static async deleteBacktest(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete backtest');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to poll for updates
|
||||
static async pollBacktestUpdates(
|
||||
id: string,
|
||||
onUpdate: (status: BacktestStatus, progress?: number, currentTime?: number) => void,
|
||||
interval: number = 1000
|
||||
): Promise<() => void> {
|
||||
let isPolling = true;
|
||||
|
||||
const poll = async () => {
|
||||
while (isPolling) {
|
||||
try {
|
||||
const { status, progress, currentTime } = await this.getBacktestStatus(id);
|
||||
onUpdate(status, progress, currentTime);
|
||||
|
||||
if (status === 'completed' || status === 'error' || status === 'stopped') {
|
||||
isPolling = false;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Polling error:', error);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
isPolling = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
export type BacktestStatus =
|
||||
| 'idle'
|
||||
| 'configured'
|
||||
| 'running'
|
||||
| 'paused'
|
||||
| 'stopped'
|
||||
| 'completed'
|
||||
| 'error';
|
||||
|
||||
export interface BacktestConfig {
|
||||
name: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
initialCapital: number;
|
||||
symbols: string[];
|
||||
strategy: string;
|
||||
speedMultiplier: number;
|
||||
commission: number;
|
||||
slippage: number;
|
||||
}
|
||||
|
||||
export interface BacktestMetrics {
|
||||
totalReturn: number;
|
||||
sharpeRatio: number;
|
||||
maxDrawdown: number;
|
||||
winRate: number;
|
||||
totalTrades: number;
|
||||
profitableTrades: number;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
averagePrice: number;
|
||||
currentPrice: number;
|
||||
realizedPnl: number;
|
||||
unrealizedPnl: number;
|
||||
lastUpdate: string;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
symbol: string;
|
||||
side: 'buy' | 'sell';
|
||||
quantity: number;
|
||||
price: number;
|
||||
commission: number;
|
||||
pnl?: number;
|
||||
}
|
||||
|
||||
export interface PerformanceDataPoint {
|
||||
timestamp: string;
|
||||
portfolioValue: number;
|
||||
pnl: number;
|
||||
drawdown: number;
|
||||
}
|
||||
|
||||
export interface BacktestResult {
|
||||
id: string;
|
||||
config: BacktestConfig;
|
||||
metrics: BacktestMetrics;
|
||||
positions: Position[];
|
||||
trades: Trade[];
|
||||
performanceData: PerformanceDataPoint[];
|
||||
}
|
||||
|
||||
export interface OrderBookLevel {
|
||||
price: number;
|
||||
size: number;
|
||||
orderCount?: number;
|
||||
}
|
||||
|
||||
export interface OrderBookSnapshot {
|
||||
symbol: string;
|
||||
timestamp: string;
|
||||
bids: OrderBookLevel[];
|
||||
asks: OrderBookLevel[];
|
||||
}
|
||||
|
||||
export interface RiskMetrics {
|
||||
currentExposure: number;
|
||||
dailyPnl: number;
|
||||
positionCount: number;
|
||||
grossExposure: number;
|
||||
maxPositionSize: number;
|
||||
utilizationPct: number;
|
||||
}
|
||||
12
apps/stock/web-app/src/features/backtest/types/index.ts
Normal file
12
apps/stock/web-app/src/features/backtest/types/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export type {
|
||||
BacktestStatus,
|
||||
BacktestConfig,
|
||||
BacktestMetrics,
|
||||
Position,
|
||||
Trade,
|
||||
PerformanceDataPoint,
|
||||
BacktestResult,
|
||||
OrderBookLevel,
|
||||
OrderBookSnapshot,
|
||||
RiskMetrics,
|
||||
} from './backtest.types';
|
||||
268
apps/stock/web-app/src/features/charts/ChartPage.tsx
Normal file
268
apps/stock/web-app/src/features/charts/ChartPage.tsx
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { SymbolChart } from './components/SymbolChart';
|
||||
import { ChartToolbar } from './components/ChartToolbar';
|
||||
import { IndicatorList } from './components/IndicatorList';
|
||||
import { IndicatorSelector } from './components/IndicatorSelector';
|
||||
import { IndicatorSettings, type IndicatorConfig } from './components/IndicatorSettings';
|
||||
import type { ChartConfig, CandlestickData, MarketData } from './types';
|
||||
|
||||
// Mock data generator for demonstration
|
||||
function generateMockData(days: number = 365): CandlestickData[] {
|
||||
const data: CandlestickData[] = [];
|
||||
const now = new Date();
|
||||
let price = 100;
|
||||
|
||||
for (let i = days; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
|
||||
// Generate realistic price movement
|
||||
const volatility = 0.02;
|
||||
const trend = Math.random() > 0.5 ? 1 : -1;
|
||||
const change = (Math.random() * volatility * 2 - volatility) * price;
|
||||
|
||||
const open = price;
|
||||
const close = price + change * trend;
|
||||
const high = Math.max(open, close) + Math.random() * volatility * price;
|
||||
const low = Math.min(open, close) - Math.random() * volatility * price;
|
||||
const volume = Math.floor(1000000 + Math.random() * 9000000);
|
||||
|
||||
data.push({
|
||||
time: Math.floor(date.getTime() / 1000), // Convert to Unix timestamp (seconds)
|
||||
open: parseFloat(open.toFixed(2)),
|
||||
high: parseFloat(high.toFixed(2)),
|
||||
low: parseFloat(low.toFixed(2)),
|
||||
close: parseFloat(close.toFixed(2)),
|
||||
volume,
|
||||
});
|
||||
|
||||
price = close;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function ChartPage() {
|
||||
const [config, setConfig] = useState<ChartConfig>({
|
||||
symbol: 'AAPL',
|
||||
interval: '1d',
|
||||
chartType: 'candlestick',
|
||||
showVolume: true,
|
||||
indicators: [],
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
const [chartData, setChartData] = useState<CandlestickData[]>([]);
|
||||
const [marketData, setMarketData] = useState<MarketData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showIndicatorSelector, setShowIndicatorSelector] = useState(false);
|
||||
const [showIndicatorSettings, setShowIndicatorSettings] = useState(false);
|
||||
const [selectedIndicator, setSelectedIndicator] = useState<IndicatorConfig | null>(null);
|
||||
const [activeIndicators, setActiveIndicators] = useState<IndicatorConfig[]>([
|
||||
{
|
||||
id: 'VOLUME',
|
||||
name: 'Volume',
|
||||
type: 'overlay',
|
||||
visible: true,
|
||||
parameters: {},
|
||||
style: {
|
||||
color: '#3b82f6',
|
||||
lineWidth: 1,
|
||||
lineStyle: 'solid',
|
||||
},
|
||||
}
|
||||
]);
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.key === '/') {
|
||||
e.preventDefault();
|
||||
setShowIndicatorSelector(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyPress);
|
||||
return () => window.removeEventListener('keydown', handleKeyPress);
|
||||
}, []);
|
||||
|
||||
// Load chart data
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate API call delay
|
||||
setTimeout(() => {
|
||||
const data = generateMockData();
|
||||
setChartData(data);
|
||||
|
||||
// Set market data from latest candle
|
||||
if (data.length > 0) {
|
||||
const latest = data[data.length - 1];
|
||||
const previous = data[data.length - 2];
|
||||
const change = latest.close - previous.close;
|
||||
const changePercent = (change / previous.close) * 100;
|
||||
|
||||
setMarketData({
|
||||
symbol: config.symbol,
|
||||
name: 'Apple Inc.',
|
||||
price: latest.close,
|
||||
change,
|
||||
changePercent,
|
||||
volume: latest.volume || 0,
|
||||
high: latest.high,
|
||||
low: latest.low,
|
||||
open: latest.open,
|
||||
previousClose: previous.close,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}, [config.symbol, config.interval]);
|
||||
|
||||
const handleConfigChange = (updates: Partial<ChartConfig>) => {
|
||||
setConfig(prev => ({ ...prev, ...updates }));
|
||||
};
|
||||
|
||||
const handleAddIndicators = (indicatorIds: string[]) => {
|
||||
// Filter out indicators that already exist
|
||||
const newIndicatorIds = indicatorIds.filter(id => !activeIndicators.find(i => i.id === id));
|
||||
|
||||
const newIndicators = newIndicatorIds.map(id => {
|
||||
const colors: Record<string, string> = {
|
||||
SMA20: '#ff9800',
|
||||
SMA50: '#2196f3',
|
||||
EMA20: '#4caf50',
|
||||
BB: '#ff5252',
|
||||
VWAP: '#9c27b0',
|
||||
RSI: '#00bcd4',
|
||||
MACD: '#795548',
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
name: id.replace(/(\d+)/, ' $1'),
|
||||
type: ['RSI', 'MACD'].includes(id) ? 'panel' : 'overlay',
|
||||
visible: true,
|
||||
parameters: {},
|
||||
style: {
|
||||
color: colors[id] || '#9e9e9e',
|
||||
lineWidth: 2,
|
||||
lineStyle: 'solid' as const,
|
||||
},
|
||||
} as IndicatorConfig;
|
||||
});
|
||||
|
||||
const updatedIndicators = [...activeIndicators, ...newIndicators];
|
||||
setActiveIndicators(updatedIndicators);
|
||||
setConfig(prev => ({ ...prev, indicators: updatedIndicators.filter(i => i.id !== 'VOLUME').map(i => i.id) }));
|
||||
};
|
||||
|
||||
const handleRemoveIndicator = (id: string) => {
|
||||
// Don't allow removing Volume indicator
|
||||
if (id === 'VOLUME') return;
|
||||
|
||||
setActiveIndicators(prev => prev.filter(i => i.id !== id));
|
||||
setConfig(prev => ({ ...prev, indicators: prev.indicators.filter(i => i !== id) }));
|
||||
};
|
||||
|
||||
const handleToggleIndicatorVisibility = (id: string) => {
|
||||
setActiveIndicators(prev => prev.map(i =>
|
||||
i.id === id ? { ...i, visible: !i.visible } : i
|
||||
));
|
||||
};
|
||||
|
||||
const handleIndicatorSettings = (id: string) => {
|
||||
const indicator = activeIndicators.find(i => i.id === id);
|
||||
if (indicator) {
|
||||
setSelectedIndicator(indicator);
|
||||
setShowIndicatorSettings(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveIndicatorSettings = (updatedConfig: IndicatorConfig) => {
|
||||
setActiveIndicators(prev => prev.map(i =>
|
||||
i.id === updatedConfig.id ? updatedConfig : i
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-shrink-0">
|
||||
<h1 className="text-lg font-bold text-text-primary mb-2 px-8 pt-4">Market Charts</h1>
|
||||
<p className="text-text-secondary mb-4 text-sm px-8">
|
||||
Real-time market data and advanced charting powered by TradingView.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex bg-surface-secondary rounded-lg border border-border mx-8 mb-8 overflow-hidden">
|
||||
{/* Left Sidebar - Indicator List */}
|
||||
{showSidebar && (
|
||||
<IndicatorList
|
||||
indicators={activeIndicators.map(i => ({
|
||||
id: i.id,
|
||||
name: i.name,
|
||||
type: i.type,
|
||||
visible: i.visible,
|
||||
color: i.style.color,
|
||||
}))}
|
||||
onRemove={handleRemoveIndicator}
|
||||
onToggleVisibility={handleToggleIndicatorVisibility}
|
||||
onSettings={handleIndicatorSettings}
|
||||
onAddIndicator={() => setShowIndicatorSelector(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main Chart Area */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<ChartToolbar config={config} onConfigChange={handleConfigChange} />
|
||||
|
||||
<div className="flex-1">
|
||||
{isLoading ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-primary-500 mx-auto mb-4"></div>
|
||||
<p className="text-sm text-text-secondary">Loading chart data...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SymbolChart
|
||||
symbol={config.symbol}
|
||||
data={chartData}
|
||||
config={{
|
||||
...config,
|
||||
indicators: activeIndicators.filter(i => i.visible && i.id !== 'VOLUME').map(i => i.id),
|
||||
showVolume: activeIndicators.find(i => i.id === 'VOLUME')?.visible ?? true,
|
||||
}}
|
||||
activeIndicators={activeIndicators}
|
||||
onIndicatorSettings={handleIndicatorSettings}
|
||||
onIndicatorRemove={handleRemoveIndicator}
|
||||
onIndicatorToggle={handleToggleIndicatorVisibility}
|
||||
onAddIndicator={() => setShowIndicatorSelector(true)}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indicator Selector Modal */}
|
||||
<IndicatorSelector
|
||||
isOpen={showIndicatorSelector}
|
||||
onClose={() => setShowIndicatorSelector(false)}
|
||||
selectedIndicators={activeIndicators.map(i => i.id)}
|
||||
onIndicatorsChange={handleAddIndicators}
|
||||
/>
|
||||
|
||||
{/* Indicator Settings Modal */}
|
||||
<IndicatorSettings
|
||||
isOpen={showIndicatorSettings}
|
||||
onClose={() => setShowIndicatorSettings(false)}
|
||||
indicator={selectedIndicator}
|
||||
onSave={handleSaveIndicatorSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { Component, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ChartErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('Chart error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full bg-surface-secondary rounded-lg border border-border p-8">
|
||||
<div className="text-center">
|
||||
<div className="text-error text-6xl mb-4">⚠️</div>
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-2">
|
||||
Chart Loading Error
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary mb-4">
|
||||
{this.state.error?.message || 'Unable to load the chart'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: null })}
|
||||
className="px-4 py-2 bg-primary-500 text-white rounded-md text-sm font-medium hover:bg-primary-600 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import { Cog6ToothIcon, XMarkIcon, EyeIcon, EyeSlashIcon, ChevronUpIcon, ChevronDownIcon, PlusIcon } from '@heroicons/react/24/outline';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface IndicatorInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
value?: number;
|
||||
color: string;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
interface ChartOverlayProps {
|
||||
symbol: string;
|
||||
price?: number;
|
||||
change?: number;
|
||||
changePercent?: number;
|
||||
indicators: IndicatorInfo[];
|
||||
onIndicatorSettings: (id: string) => void;
|
||||
onIndicatorRemove: (id: string) => void;
|
||||
onIndicatorToggle?: (id: string) => void;
|
||||
onAddIndicator?: () => void;
|
||||
}
|
||||
|
||||
export function ChartOverlay({
|
||||
symbol,
|
||||
price,
|
||||
change,
|
||||
changePercent,
|
||||
indicators,
|
||||
onIndicatorSettings,
|
||||
onIndicatorRemove,
|
||||
onIndicatorToggle,
|
||||
onAddIndicator,
|
||||
}: ChartOverlayProps) {
|
||||
const [showIndicators, setShowIndicators] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="absolute top-2 left-2 z-10 pointer-events-none">
|
||||
{/* Indicators */}
|
||||
{(indicators.length > 0 || true) && (
|
||||
<div>
|
||||
{/* Indicators Header with Toggle */}
|
||||
<div className="bg-surface-secondary/90 backdrop-blur-sm rounded-md px-2 py-0.5 shadow-sm pointer-events-auto flex items-center justify-between mb-0.5">
|
||||
<span className="text-xs text-text-secondary">Indicators</span>
|
||||
<button
|
||||
onClick={() => setShowIndicators(!showIndicators)}
|
||||
className="p-0.5 hover:bg-surface-tertiary rounded"
|
||||
>
|
||||
{showIndicators ? (
|
||||
<ChevronUpIcon className="w-3 h-3 text-text-secondary" />
|
||||
) : (
|
||||
<ChevronDownIcon className="w-3 h-3 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Indicator List */}
|
||||
{showIndicators && (
|
||||
<div className="space-y-0.5">
|
||||
{indicators.map((indicator) => (
|
||||
<div
|
||||
key={indicator.id}
|
||||
className={`bg-surface-secondary/90 backdrop-blur-sm rounded-md px-2 py-0.5 shadow-sm pointer-events-auto group flex items-center gap-1.5 ${
|
||||
indicator.visible === false ? 'opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Eye icon on the left */}
|
||||
{onIndicatorToggle && (
|
||||
<button
|
||||
onClick={() => onIndicatorToggle(indicator.id)}
|
||||
className="p-0.5 hover:bg-surface-tertiary rounded"
|
||||
title={indicator.visible === false ? "Show indicator" : "Hide indicator"}
|
||||
>
|
||||
{indicator.visible === false ? (
|
||||
<EyeIcon className="w-3 h-3 text-text-secondary" />
|
||||
) : (
|
||||
<EyeSlashIcon className="w-3 h-3 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Color dot */}
|
||||
<div
|
||||
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: indicator.color }}
|
||||
/>
|
||||
|
||||
{/* Name and value */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-text-secondary whitespace-nowrap">
|
||||
{indicator.name}
|
||||
</span>
|
||||
{indicator.value !== undefined && indicator.visible !== false && (
|
||||
<span className="text-xs font-medium text-text-primary">
|
||||
{indicator.value.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Spacer to push buttons to the right */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Settings and Remove on the right */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => onIndicatorSettings(indicator.id)}
|
||||
className="p-0.5 hover:bg-surface-tertiary rounded"
|
||||
>
|
||||
<Cog6ToothIcon className="w-3 h-3 text-text-secondary" />
|
||||
</button>
|
||||
{indicator.id !== 'VOLUME' && (
|
||||
<button
|
||||
onClick={() => onIndicatorRemove(indicator.id)}
|
||||
className="p-0.5 hover:bg-surface-tertiary rounded"
|
||||
title="Remove indicator"
|
||||
>
|
||||
<XMarkIcon className="w-3 h-3 text-text-secondary" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add Indicator Button */}
|
||||
{onAddIndicator && (
|
||||
<button
|
||||
onClick={onAddIndicator}
|
||||
className="bg-surface-secondary/90 backdrop-blur-sm rounded-md px-2 py-0.5 shadow-sm pointer-events-auto flex items-center gap-1.5 w-full hover:bg-surface-tertiary/90 transition-colors"
|
||||
title="Add indicator"
|
||||
>
|
||||
<PlusIcon className="w-3 h-3 text-text-secondary" />
|
||||
<span className="text-xs text-text-secondary">Add Indicator</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import { ChartBarIcon } from '@heroicons/react/24/outline';
|
||||
import type { ChartType, ChartInterval, ChartConfig } from '../types';
|
||||
|
||||
interface ChartToolbarProps {
|
||||
config: ChartConfig;
|
||||
onConfigChange: (config: Partial<ChartConfig>) => void;
|
||||
}
|
||||
|
||||
const intervals: { value: ChartInterval; label: string }[] = [
|
||||
{ value: '1m', label: '1m' },
|
||||
{ value: '5m', label: '5m' },
|
||||
{ value: '15m', label: '15m' },
|
||||
{ value: '30m', label: '30m' },
|
||||
{ value: '1h', label: '1H' },
|
||||
{ value: '4h', label: '4H' },
|
||||
{ value: '1d', label: '1D' },
|
||||
{ value: '1w', label: '1W' },
|
||||
{ value: '1M', label: '1M' },
|
||||
];
|
||||
|
||||
const chartTypes: { value: ChartType; label: string; icon?: typeof ChartBarIcon }[] = [
|
||||
{ value: 'candlestick', label: 'Candles' },
|
||||
{ value: 'line', label: 'Line' },
|
||||
{ value: 'area', label: 'Area' },
|
||||
{ value: 'bar', label: 'Bars' },
|
||||
];
|
||||
|
||||
export function ChartToolbar({ config, onConfigChange }: ChartToolbarProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 p-4 bg-surface-secondary border-b border-border">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Symbol Input */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={config.symbol}
|
||||
onChange={(e) => onConfigChange({ symbol: e.target.value.toUpperCase() })}
|
||||
placeholder="Symbol"
|
||||
className="px-3 py-1.5 bg-background border border-border rounded-md text-sm font-medium text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500 w-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Interval Selector */}
|
||||
<div className="flex items-center gap-1 bg-background rounded-md border border-border p-0.5">
|
||||
{intervals.map(({ value, label }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => onConfigChange({ interval: value })}
|
||||
className={`px-3 py-1 text-xs font-medium rounded transition-colors ${
|
||||
config.interval === value
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'text-text-secondary hover:text-text-primary hover:bg-surface-secondary'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Chart Type Selector */}
|
||||
<div className="flex items-center gap-1 bg-background rounded-md border border-border p-0.5">
|
||||
{chartTypes.map(({ value, label }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => onConfigChange({ chartType: value })}
|
||||
className={`px-3 py-1 text-xs font-medium rounded transition-colors ${
|
||||
config.chartType === value
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'text-text-secondary hover:text-text-primary hover:bg-surface-secondary'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Volume Toggle */}
|
||||
<button
|
||||
onClick={() => onConfigChange({ showVolume: !config.showVolume })}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-md border transition-colors ${
|
||||
config.showVolume
|
||||
? 'bg-primary-500 text-white border-primary-500'
|
||||
: 'bg-background text-text-secondary border-border hover:text-text-primary hover:border-primary-500/50'
|
||||
}`}
|
||||
>
|
||||
Volume
|
||||
</button>
|
||||
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={() => onConfigChange({ theme: config.theme === 'dark' ? 'light' : 'dark' })}
|
||||
className="px-3 py-1.5 bg-background text-text-secondary text-xs font-medium rounded-md border border-border hover:text-text-primary hover:border-primary-500/50 transition-colors"
|
||||
>
|
||||
{config.theme === 'dark' ? '☀️' : '🌙'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import { Cog6ToothIcon, XMarkIcon, EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface IndicatorItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'overlay' | 'panel';
|
||||
visible: boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface IndicatorListProps {
|
||||
indicators: IndicatorItem[];
|
||||
onRemove: (id: string) => void;
|
||||
onToggleVisibility: (id: string) => void;
|
||||
onSettings: (id: string) => void;
|
||||
onAddIndicator: () => void;
|
||||
}
|
||||
|
||||
export function IndicatorList({
|
||||
indicators,
|
||||
onRemove,
|
||||
onToggleVisibility,
|
||||
onSettings,
|
||||
onAddIndicator
|
||||
}: IndicatorListProps) {
|
||||
const [hoveredId, setHoveredId] = useState<string | null>(null);
|
||||
|
||||
// Group indicators by type
|
||||
const overlayIndicators = indicators.filter(i => i.type === 'overlay');
|
||||
const panelIndicators = indicators.filter(i => i.type === 'panel');
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-surface-secondary border-r border-border flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="p-3 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-text-primary">Indicators</h3>
|
||||
<button
|
||||
onClick={onAddIndicator}
|
||||
className="text-xs text-primary-500 hover:text-primary-400 font-medium"
|
||||
>
|
||||
+ Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indicator List */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Main Chart Section */}
|
||||
<div className="p-3">
|
||||
<div className="text-xs text-text-secondary mb-2 uppercase tracking-wider">Main Chart</div>
|
||||
|
||||
{/* Price Action */}
|
||||
<div className="bg-surface-tertiary rounded-md p-2 mb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-primary-500 rounded-full"></div>
|
||||
<span className="text-sm text-text-primary">Price</span>
|
||||
</div>
|
||||
<button className="p-1 hover:bg-surface-secondary rounded">
|
||||
<Cog6ToothIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Volume */}
|
||||
<div className="bg-surface-tertiary rounded-md p-2 mb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="text-sm text-text-primary">Volume</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button className="p-1 hover:bg-surface-secondary rounded">
|
||||
<EyeIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
</button>
|
||||
<button className="p-1 hover:bg-surface-secondary rounded">
|
||||
<Cog6ToothIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlay Indicators */}
|
||||
{overlayIndicators.map(indicator => (
|
||||
<div
|
||||
key={indicator.id}
|
||||
className="bg-surface-tertiary rounded-md p-2 mb-2 group"
|
||||
onMouseEnter={() => setHoveredId(indicator.id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: indicator.color || '#9e9e9e' }}
|
||||
></div>
|
||||
<span className="text-sm text-text-primary">{indicator.name}</span>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 transition-opacity ${
|
||||
hoveredId === indicator.id ? 'opacity-100' : 'opacity-0'
|
||||
}`}>
|
||||
<button
|
||||
onClick={() => onToggleVisibility(indicator.id)}
|
||||
className="p-1 hover:bg-surface-secondary rounded"
|
||||
>
|
||||
{indicator.visible ?
|
||||
<EyeIcon className="w-3.5 h-3.5 text-text-secondary" /> :
|
||||
<EyeSlashIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSettings(indicator.id)}
|
||||
className="p-1 hover:bg-surface-secondary rounded"
|
||||
>
|
||||
<Cog6ToothIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onRemove(indicator.id)}
|
||||
className="p-1 hover:bg-surface-secondary rounded"
|
||||
>
|
||||
<XMarkIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Panel Indicators */}
|
||||
{panelIndicators.length > 0 && (
|
||||
<div className="p-3 border-t border-border">
|
||||
<div className="text-xs text-text-secondary mb-2 uppercase tracking-wider">Separate Panels</div>
|
||||
{panelIndicators.map(indicator => (
|
||||
<div
|
||||
key={indicator.id}
|
||||
className="bg-surface-tertiary rounded-md p-2 mb-2 group"
|
||||
onMouseEnter={() => setHoveredId(indicator.id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: indicator.color || '#9e9e9e' }}
|
||||
></div>
|
||||
<span className="text-sm text-text-primary">{indicator.name}</span>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 transition-opacity ${
|
||||
hoveredId === indicator.id ? 'opacity-100' : 'opacity-0'
|
||||
}`}>
|
||||
<button
|
||||
onClick={() => onToggleVisibility(indicator.id)}
|
||||
className="p-1 hover:bg-surface-secondary rounded"
|
||||
>
|
||||
{indicator.visible ?
|
||||
<EyeIcon className="w-3.5 h-3.5 text-text-secondary" /> :
|
||||
<EyeSlashIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSettings(indicator.id)}
|
||||
className="p-1 hover:bg-surface-secondary rounded"
|
||||
>
|
||||
<Cog6ToothIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onRemove(indicator.id)}
|
||||
className="p-1 hover:bg-surface-secondary rounded"
|
||||
>
|
||||
<XMarkIcon className="w-3.5 h-3.5 text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-3 border-t border-border">
|
||||
<button
|
||||
onClick={onAddIndicator}
|
||||
className="w-full px-3 py-2 bg-primary-500/10 text-primary-500 text-sm font-medium rounded-md hover:bg-primary-500/20 transition-colors"
|
||||
>
|
||||
Add Indicator
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import { Fragment, useState } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface IndicatorOption {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'overlay' | 'oscillator';
|
||||
}
|
||||
|
||||
const availableIndicators: IndicatorOption[] = [
|
||||
{ id: 'SMA20', name: 'SMA 20', description: 'Simple Moving Average (20 periods)', category: 'overlay' },
|
||||
{ id: 'SMA50', name: 'SMA 50', description: 'Simple Moving Average (50 periods)', category: 'overlay' },
|
||||
{ id: 'EMA20', name: 'EMA 20', description: 'Exponential Moving Average (20 periods)', category: 'overlay' },
|
||||
{ id: 'BB', name: 'Bollinger Bands', description: 'Bollinger Bands (20, 2)', category: 'overlay' },
|
||||
{ id: 'VWAP', name: 'VWAP', description: 'Volume Weighted Average Price', category: 'overlay' },
|
||||
{ id: 'RSI', name: 'RSI', description: 'Relative Strength Index (14)', category: 'oscillator' },
|
||||
{ id: 'MACD', name: 'MACD', description: 'Moving Average Convergence Divergence', category: 'oscillator' },
|
||||
];
|
||||
|
||||
interface IndicatorSelectorProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectedIndicators: string[];
|
||||
onIndicatorsChange: (indicators: string[]) => void;
|
||||
}
|
||||
|
||||
export function IndicatorSelector({ isOpen, onClose, selectedIndicators, onIndicatorsChange }: IndicatorSelectorProps) {
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set(selectedIndicators.filter(id => id !== 'VOLUME')));
|
||||
|
||||
const handleToggle = (indicatorId: string) => {
|
||||
const newSelected = new Set(selected);
|
||||
if (newSelected.has(indicatorId)) {
|
||||
newSelected.delete(indicatorId);
|
||||
} else {
|
||||
newSelected.add(indicatorId);
|
||||
}
|
||||
setSelected(newSelected);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
onIndicatorsChange(Array.from(selected));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const overlayIndicators = availableIndicators.filter(i => i.category === 'overlay');
|
||||
const oscillatorIndicators = availableIndicators.filter(i => i.category === 'oscillator');
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-secondary text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="bg-surface-secondary px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Dialog.Title as="h3" className="text-lg font-semibold text-text-primary">
|
||||
Technical Indicators
|
||||
</Dialog.Title>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-md text-text-secondary hover:text-text-primary"
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Overlay Indicators */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-primary mb-3">Overlay Indicators</h4>
|
||||
<div className="space-y-2">
|
||||
{overlayIndicators.map(indicator => (
|
||||
<label
|
||||
key={indicator.id}
|
||||
className="flex items-start space-x-3 p-3 rounded-md hover:bg-surface-tertiary cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(indicator.id)}
|
||||
onChange={() => handleToggle(indicator.id)}
|
||||
className="mt-0.5 h-4 w-4 rounded border-border text-primary-500 focus:ring-primary-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-text-primary">{indicator.name}</div>
|
||||
<div className="text-xs text-text-secondary">{indicator.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Oscillator Indicators */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-primary mb-3">Oscillators (Coming Soon)</h4>
|
||||
<div className="space-y-2 opacity-50">
|
||||
{oscillatorIndicators.map(indicator => (
|
||||
<label
|
||||
key={indicator.id}
|
||||
className="flex items-start space-x-3 p-3 rounded-md cursor-not-allowed"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled
|
||||
checked={false}
|
||||
className="mt-0.5 h-4 w-4 rounded border-border text-primary-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-text-primary">{indicator.name}</div>
|
||||
<div className="text-xs text-text-secondary">{indicator.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-tertiary px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md bg-primary-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-600 sm:ml-3 sm:w-auto"
|
||||
onClick={handleApply}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-surface-secondary px-4 py-2 text-sm font-medium text-text-primary shadow-sm ring-1 ring-inset ring-border hover:bg-surface-tertiary sm:mt-0 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import { Fragment, useState, useEffect } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export interface IndicatorConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'overlay' | 'panel';
|
||||
parameters: Record<string, any>;
|
||||
style: {
|
||||
color?: string;
|
||||
lineWidth?: number;
|
||||
lineStyle?: 'solid' | 'dashed' | 'dotted';
|
||||
};
|
||||
}
|
||||
|
||||
interface IndicatorSettingsProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
indicator: IndicatorConfig | null;
|
||||
onSave: (config: IndicatorConfig) => void;
|
||||
}
|
||||
|
||||
const indicatorParameters: Record<string, Array<{ key: string; label: string; type: 'number' | 'select'; min?: number; max?: number; options?: string[] }>> = {
|
||||
SMA20: [
|
||||
{ key: 'period', label: 'Period', type: 'number', min: 1, max: 200 },
|
||||
],
|
||||
SMA50: [
|
||||
{ key: 'period', label: 'Period', type: 'number', min: 1, max: 200 },
|
||||
],
|
||||
EMA20: [
|
||||
{ key: 'period', label: 'Period', type: 'number', min: 1, max: 200 },
|
||||
],
|
||||
BB: [
|
||||
{ key: 'period', label: 'Period', type: 'number', min: 1, max: 200 },
|
||||
{ key: 'stdDev', label: 'Standard Deviation', type: 'number', min: 0.1, max: 5 },
|
||||
],
|
||||
RSI: [
|
||||
{ key: 'period', label: 'Period', type: 'number', min: 1, max: 100 },
|
||||
{ key: 'overbought', label: 'Overbought Level', type: 'number', min: 50, max: 100 },
|
||||
{ key: 'oversold', label: 'Oversold Level', type: 'number', min: 0, max: 50 },
|
||||
],
|
||||
MACD: [
|
||||
{ key: 'fastPeriod', label: 'Fast Period', type: 'number', min: 1, max: 200 },
|
||||
{ key: 'slowPeriod', label: 'Slow Period', type: 'number', min: 1, max: 200 },
|
||||
{ key: 'signalPeriod', label: 'Signal Period', type: 'number', min: 1, max: 200 },
|
||||
],
|
||||
};
|
||||
|
||||
const defaultParameters: Record<string, Record<string, any>> = {
|
||||
SMA20: { period: 20 },
|
||||
SMA50: { period: 50 },
|
||||
EMA20: { period: 20 },
|
||||
BB: { period: 20, stdDev: 2 },
|
||||
RSI: { period: 14, overbought: 70, oversold: 30 },
|
||||
MACD: { fastPeriod: 12, slowPeriod: 26, signalPeriod: 9 },
|
||||
VWAP: {},
|
||||
};
|
||||
|
||||
const lineStyles = [
|
||||
{ value: 'solid', label: 'Solid' },
|
||||
{ value: 'dashed', label: 'Dashed' },
|
||||
{ value: 'dotted', label: 'Dotted' },
|
||||
];
|
||||
|
||||
export function IndicatorSettings({ isOpen, onClose, indicator, onSave }: IndicatorSettingsProps) {
|
||||
const [config, setConfig] = useState<IndicatorConfig | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (indicator) {
|
||||
setConfig({
|
||||
...indicator,
|
||||
parameters: { ...defaultParameters[indicator.id], ...indicator.parameters },
|
||||
});
|
||||
}
|
||||
}, [indicator]);
|
||||
|
||||
if (!config || !indicator) return null;
|
||||
|
||||
const handleParameterChange = (key: string, value: any) => {
|
||||
setConfig({
|
||||
...config,
|
||||
parameters: {
|
||||
...config.parameters,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleStyleChange = (key: string, value: any) => {
|
||||
setConfig({
|
||||
...config,
|
||||
style: {
|
||||
...config.style,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(config);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const parameters = indicatorParameters[indicator.id] || [];
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-secondary text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-md">
|
||||
<div className="bg-surface-secondary px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Dialog.Title as="h3" className="text-lg font-semibold text-text-primary">
|
||||
{config.name} Settings
|
||||
</Dialog.Title>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-md text-text-secondary hover:text-text-primary"
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Parameters */}
|
||||
{parameters.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-primary mb-3">Parameters</h4>
|
||||
<div className="space-y-3">
|
||||
{parameters.map(param => (
|
||||
<div key={param.key}>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
{param.label}
|
||||
</label>
|
||||
{param.type === 'number' ? (
|
||||
<input
|
||||
type="number"
|
||||
min={param.min}
|
||||
max={param.max}
|
||||
value={config.parameters[param.key] || 0}
|
||||
onChange={(e) => handleParameterChange(param.key, parseFloat(e.target.value))}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
/>
|
||||
) : (
|
||||
<select
|
||||
value={config.parameters[param.key] || ''}
|
||||
onChange={(e) => handleParameterChange(param.key, e.target.value)}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
>
|
||||
{param.options?.map(option => (
|
||||
<option key={option} value={option}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Style */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-primary mb-3">Style</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">Color</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={config.style.color || '#3b82f6'}
|
||||
onChange={(e) => handleStyleChange('color', e.target.value)}
|
||||
className="w-10 h-10 rounded border border-border cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={config.style.color || '#3b82f6'}
|
||||
onChange={(e) => handleStyleChange('color', e.target.value)}
|
||||
className="flex-1 px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">Line Width</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="5"
|
||||
value={config.style.lineWidth || 2}
|
||||
onChange={(e) => handleStyleChange('lineWidth', parseInt(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-center text-sm text-text-secondary mt-1">
|
||||
{config.style.lineWidth || 2}px
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">Line Style</label>
|
||||
<select
|
||||
value={config.style.lineStyle || 'solid'}
|
||||
onChange={(e) => handleStyleChange('lineStyle', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
>
|
||||
{lineStyles.map(style => (
|
||||
<option key={style.value} value={style.value}>{style.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-tertiary px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md bg-primary-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-600 sm:ml-3 sm:w-auto"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-surface-secondary px-4 py-2 text-sm font-medium text-text-primary shadow-sm ring-1 ring-inset ring-border hover:bg-surface-tertiary sm:mt-0 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/24/solid';
|
||||
import type { MarketData } from '../types';
|
||||
|
||||
interface MarketOverviewProps {
|
||||
data: MarketData | null;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function MarketOverview({ data, loading }: MarketOverviewProps) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<div className="p-4 bg-surface-secondary border-b border-border">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-6 w-32 bg-surface-tertiary rounded mb-2"></div>
|
||||
<div className="h-8 w-24 bg-surface-tertiary rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isPositive = data.change >= 0;
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-surface-secondary border-b border-border">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h2 className="text-xl font-bold text-text-primary">{data.symbol}</h2>
|
||||
<span className="text-sm text-text-secondary">{data.name}</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="text-3xl font-bold text-text-primary">
|
||||
${data.price.toFixed(2)}
|
||||
</span>
|
||||
<div className={`flex items-center gap-1 ${isPositive ? 'text-success' : 'text-error'}`}>
|
||||
{isPositive ? (
|
||||
<ArrowUpIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<ArrowDownIcon className="w-4 h-4" />
|
||||
)}
|
||||
<span className="text-lg font-medium">
|
||||
${Math.abs(data.change).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-lg font-medium">
|
||||
({isPositive ? '+' : ''}{data.changePercent.toFixed(2)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-text-muted">Open</span>
|
||||
<p className="text-text-primary font-medium">${data.open.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">High</span>
|
||||
<p className="text-text-primary font-medium">${data.high.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Low</span>
|
||||
<p className="text-text-primary font-medium">${data.low.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Prev Close</span>
|
||||
<p className="text-text-primary font-medium">${data.previousClose.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Volume</span>
|
||||
<p className="text-text-primary font-medium">
|
||||
{(data.volume / 1000000).toFixed(2)}M
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Time</span>
|
||||
<p className="text-text-primary font-medium">
|
||||
{new Date(data.timestamp).toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
import * as LightweightCharts from 'lightweight-charts';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { CandlestickData, ChartConfig, ChartOptions } from '../types';
|
||||
import { calculateBollingerBands, calculateEMA, calculateSMA, calculateVWAP } from '../utils/indicators';
|
||||
import { ChartOverlay } from './ChartOverlay';
|
||||
|
||||
interface SymbolChartProps {
|
||||
symbol: string;
|
||||
data: CandlestickData[];
|
||||
config?: Partial<ChartConfig>;
|
||||
options?: ChartOptions;
|
||||
className?: string;
|
||||
onIndicatorSettings?: (id: string) => void;
|
||||
onIndicatorRemove?: (id: string) => void;
|
||||
onIndicatorToggle?: (id: string) => void;
|
||||
onAddIndicator?: () => void;
|
||||
activeIndicators?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
visible: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function SymbolChart({
|
||||
symbol,
|
||||
data,
|
||||
config = {},
|
||||
options = {},
|
||||
className = '',
|
||||
onIndicatorSettings,
|
||||
onIndicatorRemove,
|
||||
onIndicatorToggle,
|
||||
onAddIndicator,
|
||||
activeIndicators = [],
|
||||
}: SymbolChartProps) {
|
||||
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||
const chartRef = useRef<LightweightCharts.IChartApi | null>(null);
|
||||
const seriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
|
||||
const volumeSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
|
||||
const indicatorSeriesRef = useRef<Map<string, LightweightCharts.ISeriesApi<any>>>(new Map());
|
||||
const [chartInitialized, setChartInitialized] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [currentPrice, setCurrentPrice] = useState<{ price: number; change: number; changePercent: number } | null>(null);
|
||||
const [indicatorValues, setIndicatorValues] = useState<Map<string, number>>(new Map());
|
||||
|
||||
// Default configuration
|
||||
const chartConfig: ChartConfig = {
|
||||
symbol,
|
||||
interval: '1d',
|
||||
chartType: 'candlestick',
|
||||
showVolume: true,
|
||||
indicators: [],
|
||||
theme: 'dark',
|
||||
...config,
|
||||
};
|
||||
|
||||
// Set initial price from last candle
|
||||
useEffect(() => {
|
||||
if (data.length > 0 && !currentPrice) {
|
||||
const latest = data[data.length - 1];
|
||||
const previous = data.length > 1 ? data[data.length - 2] : latest;
|
||||
const change = latest.close - previous.close;
|
||||
const changePercent = (change / previous.close) * 100;
|
||||
setCurrentPrice({ price: latest.close, change, changePercent });
|
||||
}
|
||||
}, [data, currentPrice]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartContainerRef.current) return;
|
||||
|
||||
// Create chart
|
||||
const chart = LightweightCharts.createChart(chartContainerRef.current, {
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
height: chartContainerRef.current.clientHeight || 500,
|
||||
layout: {
|
||||
background: { type: LightweightCharts.ColorType.Solid, color: chartConfig.theme === 'dark' ? '#0f0f0f' : '#ffffff' },
|
||||
textColor: chartConfig.theme === 'dark' ? '#d1d5db' : '#374151',
|
||||
},
|
||||
grid: {
|
||||
vertLines: {
|
||||
color: chartConfig.theme === 'dark' ? '#1f2937' : '#e5e7eb',
|
||||
visible: true,
|
||||
},
|
||||
horzLines: {
|
||||
color: chartConfig.theme === 'dark' ? '#1f2937' : '#e5e7eb',
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderColor: chartConfig.theme === 'dark' ? '#1f2937' : '#e5e7eb',
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: chartConfig.theme === 'dark' ? '#1f2937' : '#e5e7eb',
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
},
|
||||
});
|
||||
|
||||
chartRef.current = chart;
|
||||
|
||||
// Clear previous indicator series references
|
||||
indicatorSeriesRef.current.clear();
|
||||
|
||||
// Create main series based on chart type
|
||||
if (chartConfig.chartType === 'candlestick') {
|
||||
seriesRef.current = chart.addCandlestickSeries({
|
||||
upColor: '#10b981',
|
||||
downColor: '#ef4444',
|
||||
borderUpColor: '#10b981',
|
||||
borderDownColor: '#ef4444',
|
||||
wickUpColor: '#10b981',
|
||||
wickDownColor: '#ef4444',
|
||||
});
|
||||
} else if (chartConfig.chartType === 'line') {
|
||||
seriesRef.current = chart.addLineSeries({
|
||||
color: '#3b82f6',
|
||||
lineWidth: 2,
|
||||
});
|
||||
} else if (chartConfig.chartType === 'area') {
|
||||
seriesRef.current = chart.addAreaSeries({
|
||||
lineColor: '#3b82f6',
|
||||
topColor: '#3b82f6',
|
||||
bottomColor: 'rgba(59, 130, 246, 0.1)',
|
||||
lineWidth: 2,
|
||||
});
|
||||
} else if (chartConfig.chartType === 'bar') {
|
||||
seriesRef.current = chart.addBarSeries({
|
||||
upColor: '#10b981',
|
||||
downColor: '#ef4444',
|
||||
});
|
||||
}
|
||||
|
||||
// Create volume series if enabled
|
||||
if (chartConfig.showVolume && data.some(d => d.volume)) {
|
||||
volumeSeriesRef.current = chart.addHistogramSeries({
|
||||
color: '#3b82f680',
|
||||
priceFormat: {
|
||||
type: 'volume',
|
||||
},
|
||||
priceScaleId: 'volume',
|
||||
});
|
||||
volumeSeriesRef.current.priceScale().applyOptions({
|
||||
scaleMargins: {
|
||||
top: 0.9, // highest point of the series will be 70% away from the top
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Set data
|
||||
if (seriesRef.current && data.length > 0) {
|
||||
if (chartConfig.chartType === 'candlestick' || chartConfig.chartType === 'bar') {
|
||||
seriesRef.current.setData(data);
|
||||
} else if (chartConfig.chartType === 'line' || chartConfig.chartType === 'area') {
|
||||
// Convert candlestick data to line/area data (using close price)
|
||||
const lineData = data.map(d => ({ time: d.time, value: d.close }));
|
||||
seriesRef.current.setData(lineData);
|
||||
}
|
||||
|
||||
// Set volume data if available
|
||||
if (volumeSeriesRef.current) {
|
||||
const volumeData = data
|
||||
.filter(d => d.volume)
|
||||
.map(d => ({
|
||||
time: d.time,
|
||||
value: d.volume!,
|
||||
color: d.close >= d.open ? '#10b98180' : '#ef444480',
|
||||
}));
|
||||
volumeSeriesRef.current.setData(volumeData);
|
||||
}
|
||||
|
||||
// Add indicators - include all indicators from activeIndicators, not just visible ones
|
||||
if (activeIndicators && activeIndicators.length > 0) {
|
||||
activeIndicators.filter(ind => ind.id !== 'VOLUME').forEach(indicator => {
|
||||
const indicatorId = indicator.id;
|
||||
if (indicatorId === 'SMA20') {
|
||||
const sma20 = calculateSMA(data, 20);
|
||||
const sma20Series = chart.addLineSeries({
|
||||
color: indicator.style?.color || '#ff9800',
|
||||
lineWidth: indicator.style?.lineWidth || 2,
|
||||
title: 'SMA 20',
|
||||
visible: indicator.visible,
|
||||
});
|
||||
sma20Series.setData(sma20);
|
||||
indicatorSeriesRef.current.set('SMA20', sma20Series);
|
||||
}
|
||||
|
||||
if (indicatorId === 'SMA50') {
|
||||
const sma50 = calculateSMA(data, 50);
|
||||
const sma50Series = chart.addLineSeries({
|
||||
color: indicator.style?.color || '#2196f3',
|
||||
lineWidth: indicator.style?.lineWidth || 2,
|
||||
title: 'SMA 50',
|
||||
visible: indicator.visible,
|
||||
});
|
||||
sma50Series.setData(sma50);
|
||||
indicatorSeriesRef.current.set('SMA50', sma50Series);
|
||||
}
|
||||
|
||||
if (indicatorId === 'EMA20') {
|
||||
const ema20 = calculateEMA(data, 20);
|
||||
const ema20Series = chart.addLineSeries({
|
||||
color: indicator.style?.color || '#4caf50',
|
||||
lineWidth: indicator.style?.lineWidth || 2,
|
||||
title: 'EMA 20',
|
||||
visible: indicator.visible,
|
||||
});
|
||||
ema20Series.setData(ema20);
|
||||
indicatorSeriesRef.current.set('EMA20', ema20Series);
|
||||
}
|
||||
|
||||
if (indicatorId === 'BB') {
|
||||
const bb = calculateBollingerBands(data, 20, 2);
|
||||
|
||||
const bbUpperSeries = chart.addLineSeries({
|
||||
color: indicator.style?.color || '#ff5252',
|
||||
lineWidth: 1,
|
||||
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||
title: 'BB Upper',
|
||||
visible: indicator.visible,
|
||||
});
|
||||
bbUpperSeries.setData(bb.upper);
|
||||
indicatorSeriesRef.current.set('BB_upper', bbUpperSeries);
|
||||
|
||||
const bbMiddleSeries = chart.addLineSeries({
|
||||
color: '#9e9e9e',
|
||||
lineWidth: 1,
|
||||
title: 'BB Middle',
|
||||
visible: indicator.visible,
|
||||
});
|
||||
bbMiddleSeries.setData(bb.middle);
|
||||
indicatorSeriesRef.current.set('BB_middle', bbMiddleSeries);
|
||||
|
||||
const bbLowerSeries = chart.addLineSeries({
|
||||
color: '#4caf50',
|
||||
lineWidth: 1,
|
||||
lineStyle: LightweightCharts.LineStyle.Dashed,
|
||||
title: 'BB Lower',
|
||||
visible: indicator.visible,
|
||||
});
|
||||
bbLowerSeries.setData(bb.lower);
|
||||
indicatorSeriesRef.current.set('BB_lower', bbLowerSeries);
|
||||
}
|
||||
|
||||
if (indicatorId === 'VWAP') {
|
||||
const vwap = calculateVWAP(data);
|
||||
const vwapSeries = chart.addLineSeries({
|
||||
color: indicator.style?.color || '#9c27b0',
|
||||
lineWidth: indicator.style?.lineWidth || 2,
|
||||
title: 'VWAP',
|
||||
visible: indicator.visible,
|
||||
});
|
||||
vwapSeries.setData(vwap);
|
||||
indicatorSeriesRef.current.set('VWAP', vwapSeries);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to crosshair move for tracking values
|
||||
chart.subscribeCrosshairMove((param) => {
|
||||
if (!param || !param.time || !param.seriesData) return;
|
||||
|
||||
// Update price info
|
||||
const mainSeriesData = param.seriesData.get(seriesRef.current!);
|
||||
if (mainSeriesData) {
|
||||
const currentData = mainSeriesData as any;
|
||||
const price = currentData.close || currentData.value || 0;
|
||||
|
||||
// Find previous close
|
||||
const currentIndex = data.findIndex(d => d.time === param.time);
|
||||
const previousClose = currentIndex > 0 ? data[currentIndex - 1].close : price;
|
||||
const change = price - previousClose;
|
||||
const changePercent = (change / previousClose) * 100;
|
||||
|
||||
setCurrentPrice({ price, change, changePercent });
|
||||
}
|
||||
|
||||
// Update indicator values
|
||||
const newIndicatorValues = new Map<string, number>();
|
||||
indicatorSeriesRef.current.forEach((series, indicatorId) => {
|
||||
const indicatorData = param.seriesData.get(series);
|
||||
if (indicatorData && 'value' in indicatorData) {
|
||||
newIndicatorValues.set(indicatorId, indicatorData.value);
|
||||
}
|
||||
});
|
||||
setIndicatorValues(newIndicatorValues);
|
||||
});
|
||||
|
||||
// Fit content only on initial load
|
||||
chart.timeScale().fitContent();
|
||||
setIsLoading(false);
|
||||
setChartInitialized(true);
|
||||
}
|
||||
|
||||
// Handle resize
|
||||
const handleResize = () => {
|
||||
if (chartContainerRef.current && chart) {
|
||||
chart.applyOptions({
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
height: chartContainerRef.current.clientHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (chart) {
|
||||
chart.remove();
|
||||
}
|
||||
};
|
||||
}, [data.length, chartConfig.theme, chartConfig.chartType, activeIndicators.length]); // Include activeIndicators.length to recreate when indicators are added/removed
|
||||
|
||||
// Update data when it changes
|
||||
useEffect(() => {
|
||||
if (!seriesRef.current || !data.length) return;
|
||||
|
||||
if (chartConfig.chartType === 'candlestick' || chartConfig.chartType === 'bar') {
|
||||
seriesRef.current.setData(data);
|
||||
} else if (chartConfig.chartType === 'line' || chartConfig.chartType === 'area') {
|
||||
const lineData = data.map(d => ({ time: d.time, value: d.close }));
|
||||
seriesRef.current.setData(lineData);
|
||||
}
|
||||
|
||||
if (volumeSeriesRef.current) {
|
||||
const volumeData = data
|
||||
.filter(d => d.volume)
|
||||
.map(d => ({
|
||||
time: d.time,
|
||||
value: d.volume!,
|
||||
color: d.close >= d.open ? '#10b98180' : '#ef444480',
|
||||
}));
|
||||
volumeSeriesRef.current.setData(volumeData);
|
||||
}
|
||||
|
||||
// Don't fit content on data updates to preserve user's zoom/pan
|
||||
// chartRef.current?.timeScale().fitContent();
|
||||
}, [data, chartConfig.chartType]);
|
||||
|
||||
// Create a dependency string that captures visibility changes
|
||||
const visibilityKey = activeIndicators.map(i => `${i.id}:${i.visible}`).join(',');
|
||||
|
||||
// Handle indicator visibility changes without recreating chart
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
// Small delay to ensure chart is fully initialized
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Update volume visibility
|
||||
if (volumeSeriesRef.current) {
|
||||
const volumeVisible = chartConfig.showVolume;
|
||||
volumeSeriesRef.current.applyOptions({
|
||||
visible: volumeVisible,
|
||||
});
|
||||
}
|
||||
|
||||
// Update indicator visibility
|
||||
activeIndicators.forEach(indicator => {
|
||||
if (indicator.id === 'VOLUME') return; // Skip volume, handled above
|
||||
|
||||
// For indicators with multiple series (like Bollinger Bands)
|
||||
if (indicator.id === 'BB') {
|
||||
['BB_upper', 'BB_middle', 'BB_lower'].forEach(seriesId => {
|
||||
const series = indicatorSeriesRef.current.get(seriesId);
|
||||
if (series) {
|
||||
series.applyOptions({ visible: indicator.visible });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// For single series indicators
|
||||
const series = indicatorSeriesRef.current.get(indicator.id);
|
||||
if (series) {
|
||||
series.applyOptions({ visible: indicator.visible });
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [chartConfig.showVolume, visibilityKey]);
|
||||
|
||||
// Prepare indicator info for overlay - include all indicators
|
||||
const indicatorInfo = activeIndicators.map(indicator => ({
|
||||
id: indicator.id,
|
||||
name: indicator.name,
|
||||
value: indicator.visible ? indicatorValues.get(indicator.id) : undefined,
|
||||
color: indicator.color,
|
||||
visible: indicator.visible,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-10">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-primary-500"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chart container */}
|
||||
<div ref={chartContainerRef} style={{ width: '100%', height: '100%' }} />
|
||||
|
||||
{/* Chart Overlay with indicators */}
|
||||
{!isLoading && onIndicatorSettings && onIndicatorRemove && (
|
||||
<ChartOverlay
|
||||
symbol={symbol}
|
||||
price={currentPrice?.price}
|
||||
change={currentPrice?.change}
|
||||
changePercent={currentPrice?.changePercent}
|
||||
indicators={indicatorInfo}
|
||||
onIndicatorSettings={onIndicatorSettings}
|
||||
onIndicatorRemove={onIndicatorRemove}
|
||||
onIndicatorToggle={onIndicatorToggle}
|
||||
onAddIndicator={onAddIndicator}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export { SymbolChart } from './SymbolChart';
|
||||
export { ChartToolbar } from './ChartToolbar';
|
||||
export { MarketOverview } from './MarketOverview';
|
||||
export { ChartErrorBoundary } from './ChartErrorBoundary';
|
||||
export { IndicatorSelector } from './IndicatorSelector';
|
||||
export { IndicatorList } from './IndicatorList';
|
||||
export { IndicatorSettings } from './IndicatorSettings';
|
||||
export { ChartOverlay } from './ChartOverlay';
|
||||
1
apps/stock/web-app/src/features/charts/hooks/index.ts
Normal file
1
apps/stock/web-app/src/features/charts/hooks/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { useChartData } from './useChartData';
|
||||
145
apps/stock/web-app/src/features/charts/hooks/useChartData.ts
Normal file
145
apps/stock/web-app/src/features/charts/hooks/useChartData.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { ChartDataService } from '../services/chartDataService';
|
||||
import type { CandlestickData, MarketData, ChartInterval } from '../types';
|
||||
|
||||
interface UseChartDataOptions {
|
||||
symbol: string;
|
||||
interval: ChartInterval;
|
||||
enableRealtime?: boolean;
|
||||
}
|
||||
|
||||
export function useChartData({ symbol, interval, enableRealtime = true }: UseChartDataOptions) {
|
||||
const [data, setData] = useState<CandlestickData[]>([]);
|
||||
const [marketData, setMarketData] = useState<MarketData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||
|
||||
// Fetch historical data
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const { start, end } = ChartDataService.getDefaultDateRange(interval);
|
||||
const historicalData = await ChartDataService.getHistoricalData(
|
||||
symbol,
|
||||
interval,
|
||||
start,
|
||||
end
|
||||
);
|
||||
|
||||
if (!cancelled) {
|
||||
setData(historicalData);
|
||||
}
|
||||
|
||||
// Fetch current market data
|
||||
const currentData = await ChartDataService.getRealtimeData(symbol);
|
||||
if (!cancelled) {
|
||||
setMarketData(currentData);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load chart data');
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [symbol, interval]);
|
||||
|
||||
// Subscribe to real-time updates
|
||||
useEffect(() => {
|
||||
if (!enableRealtime || isLoading || error) return;
|
||||
|
||||
const handleRealtimeUpdate = (newCandle: CandlestickData) => {
|
||||
setData(prevData => {
|
||||
const lastCandle = prevData[prevData.length - 1];
|
||||
|
||||
// Check if this is an update to the current candle or a new one
|
||||
if (lastCandle && lastCandle.time === newCandle.time) {
|
||||
// Update existing candle
|
||||
return [...prevData.slice(0, -1), newCandle];
|
||||
} else {
|
||||
// Add new candle
|
||||
return [...prevData, newCandle];
|
||||
}
|
||||
});
|
||||
|
||||
// Update market data
|
||||
setMarketData(prev => {
|
||||
if (!prev) return prev;
|
||||
|
||||
const change = newCandle.close - (prev.previousClose || prev.price);
|
||||
const changePercent = (change / (prev.previousClose || prev.price)) * 100;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
price: newCandle.close,
|
||||
change,
|
||||
changePercent,
|
||||
high: Math.max(prev.high, newCandle.high),
|
||||
low: Math.min(prev.low, newCandle.low),
|
||||
volume: prev.volume + (newCandle.volume || 0),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleError = (error: Error) => {
|
||||
console.error('Real-time data error:', error);
|
||||
};
|
||||
|
||||
unsubscribeRef.current = ChartDataService.subscribeToRealtime(
|
||||
symbol,
|
||||
handleRealtimeUpdate,
|
||||
handleError
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [symbol, enableRealtime, isLoading, error]);
|
||||
|
||||
const refresh = async () => {
|
||||
const { start, end } = ChartDataService.getDefaultDateRange(interval);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const newData = await ChartDataService.getHistoricalData(symbol, interval, start, end);
|
||||
setData(newData);
|
||||
|
||||
const currentData = await ChartDataService.getRealtimeData(symbol);
|
||||
setMarketData(currentData);
|
||||
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to refresh data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
data,
|
||||
marketData,
|
||||
isLoading,
|
||||
error,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
3
apps/stock/web-app/src/features/charts/index.ts
Normal file
3
apps/stock/web-app/src/features/charts/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { ChartPage } from './ChartPage';
|
||||
export * from './types';
|
||||
export * from './components';
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
import type { CandlestickData, MarketData, ChartInterval } from '../types';
|
||||
|
||||
const API_BASE = '/api/market-data';
|
||||
|
||||
export class ChartDataService {
|
||||
static async getHistoricalData(
|
||||
symbol: string,
|
||||
interval: ChartInterval,
|
||||
startDate?: Date,
|
||||
endDate?: Date
|
||||
): Promise<CandlestickData[]> {
|
||||
const params = new URLSearchParams({
|
||||
symbol,
|
||||
interval,
|
||||
...(startDate && { start: startDate.toISOString() }),
|
||||
...(endDate && { end: endDate.toISOString() }),
|
||||
});
|
||||
|
||||
const response = await fetch(`${API_BASE}/historical?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch historical data');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Transform data to match lightweight-charts format
|
||||
const candlestickData = data.map((candle: any) => {
|
||||
// Convert time to Unix timestamp in seconds
|
||||
let time: number;
|
||||
const rawTime = candle.timestamp || candle.time;
|
||||
|
||||
if (typeof rawTime === 'string') {
|
||||
// If it's a date string, parse it
|
||||
time = Math.floor(new Date(rawTime).getTime() / 1000);
|
||||
} else if (typeof rawTime === 'number') {
|
||||
// If it's already a number, check if it's in milliseconds or seconds
|
||||
time = rawTime > 9999999999 ? Math.floor(rawTime / 1000) : rawTime;
|
||||
} else {
|
||||
// Default to current time if invalid
|
||||
time = Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
time,
|
||||
open: candle.open,
|
||||
high: candle.high,
|
||||
low: candle.low,
|
||||
close: candle.close,
|
||||
volume: candle.volume,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by time and remove duplicates
|
||||
const sortedData = candlestickData.sort((a, b) => a.time - b.time);
|
||||
|
||||
// Remove duplicates by keeping only the last entry for each timestamp
|
||||
const uniqueData = sortedData.reduce((acc: CandlestickData[], current) => {
|
||||
const lastItem = acc[acc.length - 1];
|
||||
|
||||
if (!lastItem || current.time !== lastItem.time) {
|
||||
acc.push(current);
|
||||
} else {
|
||||
// Replace with current if duplicate timestamp
|
||||
acc[acc.length - 1] = current;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return uniqueData;
|
||||
}
|
||||
|
||||
static async getRealtimeData(symbol: string): Promise<MarketData> {
|
||||
const response = await fetch(`${API_BASE}/quote/${symbol}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch market data');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
static async searchSymbols(query: string): Promise<Array<{ symbol: string; name: string; exchange: string }>> {
|
||||
const response = await fetch(`${API_BASE}/search?q=${encodeURIComponent(query)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to search symbols');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// WebSocket connection for real-time updates
|
||||
static subscribeToRealtime(
|
||||
symbol: string,
|
||||
onUpdate: (data: CandlestickData) => void,
|
||||
onError?: (error: Error) => void
|
||||
): () => void {
|
||||
const ws = new WebSocket(`ws://localhost:3000/ws/market-data`);
|
||||
let isConnected = false;
|
||||
|
||||
ws.onopen = () => {
|
||||
isConnected = true;
|
||||
// Subscribe to symbol
|
||||
ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
symbol,
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.symbol === symbol) {
|
||||
// Convert timestamp to Unix seconds
|
||||
let time: number;
|
||||
if (typeof data.timestamp === 'string') {
|
||||
time = Math.floor(new Date(data.timestamp).getTime() / 1000);
|
||||
} else if (typeof data.timestamp === 'number') {
|
||||
time = data.timestamp > 9999999999 ? Math.floor(data.timestamp / 1000) : data.timestamp;
|
||||
} else {
|
||||
time = Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
onUpdate({
|
||||
time,
|
||||
open: data.open,
|
||||
high: data.high,
|
||||
low: data.low,
|
||||
close: data.close,
|
||||
volume: data.volume,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
onError?.(error as Error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
onError?.(new Error('WebSocket error'));
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
isConnected = false;
|
||||
};
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
if (isConnected) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'unsubscribe',
|
||||
symbol,
|
||||
}));
|
||||
}
|
||||
ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
// Convert interval to milliseconds for calculations
|
||||
static intervalToMs(interval: ChartInterval): number {
|
||||
const map: Record<ChartInterval, number> = {
|
||||
'1m': 60 * 1000,
|
||||
'5m': 5 * 60 * 1000,
|
||||
'15m': 15 * 60 * 1000,
|
||||
'30m': 30 * 60 * 1000,
|
||||
'1h': 60 * 60 * 1000,
|
||||
'4h': 4 * 60 * 60 * 1000,
|
||||
'1d': 24 * 60 * 60 * 1000,
|
||||
'1w': 7 * 24 * 60 * 60 * 1000,
|
||||
'1M': 30 * 24 * 60 * 60 * 1000,
|
||||
};
|
||||
return map[interval] || map['1d'];
|
||||
}
|
||||
|
||||
// Get appropriate date range for interval
|
||||
static getDefaultDateRange(interval: ChartInterval): { start: Date; end: Date } {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
|
||||
switch (interval) {
|
||||
case '1m':
|
||||
case '5m':
|
||||
start.setHours(start.getHours() - 24); // 24 hours
|
||||
break;
|
||||
case '15m':
|
||||
case '30m':
|
||||
start.setDate(start.getDate() - 7); // 1 week
|
||||
break;
|
||||
case '1h':
|
||||
start.setDate(start.getDate() - 30); // 1 month
|
||||
break;
|
||||
case '4h':
|
||||
start.setMonth(start.getMonth() - 3); // 3 months
|
||||
break;
|
||||
case '1d':
|
||||
start.setFullYear(start.getFullYear() - 1); // 1 year
|
||||
break;
|
||||
case '1w':
|
||||
start.setFullYear(start.getFullYear() - 3); // 3 years
|
||||
break;
|
||||
case '1M':
|
||||
start.setFullYear(start.getFullYear() - 10); // 10 years
|
||||
break;
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
}
|
||||
118
apps/stock/web-app/src/features/charts/types/index.ts
Normal file
118
apps/stock/web-app/src/features/charts/types/index.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
export interface CandlestickData {
|
||||
time: number; // Unix timestamp in seconds
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
export interface LineData {
|
||||
time: number; // Unix timestamp in seconds
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface VolumeData {
|
||||
time: number; // Unix timestamp in seconds
|
||||
value: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export type ChartType = 'candlestick' | 'line' | 'area' | 'bar';
|
||||
|
||||
export type ChartInterval =
|
||||
| '1m'
|
||||
| '5m'
|
||||
| '15m'
|
||||
| '30m'
|
||||
| '1h'
|
||||
| '4h'
|
||||
| '1d'
|
||||
| '1w'
|
||||
| '1M';
|
||||
|
||||
export interface ChartConfig {
|
||||
symbol: string;
|
||||
interval: ChartInterval;
|
||||
chartType: ChartType;
|
||||
showVolume: boolean;
|
||||
indicators: string[];
|
||||
theme: 'light' | 'dark';
|
||||
}
|
||||
|
||||
export interface MarketData {
|
||||
symbol: string;
|
||||
name: string;
|
||||
price: number;
|
||||
change: number;
|
||||
changePercent: number;
|
||||
volume: number;
|
||||
high: number;
|
||||
low: number;
|
||||
open: number;
|
||||
previousClose: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface ChartIndicator {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'overlay' | 'separate';
|
||||
parameters: Record<string, number>;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
// TradingView Lightweight Charts specific types
|
||||
export interface ChartOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
layout?: {
|
||||
background?: {
|
||||
type: 'solid' | 'gradient';
|
||||
color?: string;
|
||||
};
|
||||
textColor?: string;
|
||||
fontSize?: number;
|
||||
};
|
||||
grid?: {
|
||||
vertLines?: {
|
||||
color?: string;
|
||||
style?: number;
|
||||
visible?: boolean;
|
||||
};
|
||||
horzLines?: {
|
||||
color?: string;
|
||||
style?: number;
|
||||
visible?: boolean;
|
||||
};
|
||||
};
|
||||
crosshair?: {
|
||||
mode?: number;
|
||||
vertLine?: {
|
||||
color?: string;
|
||||
width?: number;
|
||||
style?: number;
|
||||
visible?: boolean;
|
||||
labelVisible?: boolean;
|
||||
};
|
||||
horzLine?: {
|
||||
color?: string;
|
||||
width?: number;
|
||||
style?: number;
|
||||
visible?: boolean;
|
||||
labelVisible?: boolean;
|
||||
};
|
||||
};
|
||||
timeScale?: {
|
||||
rightOffset?: number;
|
||||
barSpacing?: number;
|
||||
fixLeftEdge?: boolean;
|
||||
lockVisibleTimeRangeOnResize?: boolean;
|
||||
rightBarStaysOnScroll?: boolean;
|
||||
borderVisible?: boolean;
|
||||
borderColor?: string;
|
||||
visible?: boolean;
|
||||
timeVisible?: boolean;
|
||||
secondsVisible?: boolean;
|
||||
};
|
||||
}
|
||||
258
apps/stock/web-app/src/features/charts/utils/indicators.ts
Normal file
258
apps/stock/web-app/src/features/charts/utils/indicators.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import type { CandlestickData, LineData } from '../types';
|
||||
|
||||
// Simple Moving Average
|
||||
export function calculateSMA(data: CandlestickData[], period: number): LineData[] {
|
||||
const result: LineData[] = [];
|
||||
|
||||
for (let i = period - 1; i < data.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sum += data[i - j].close;
|
||||
}
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
value: parseFloat((sum / period).toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Exponential Moving Average
|
||||
export function calculateEMA(data: CandlestickData[], period: number): LineData[] {
|
||||
const result: LineData[] = [];
|
||||
const multiplier = 2 / (period + 1);
|
||||
|
||||
// Start with SMA for the first period
|
||||
let sum = 0;
|
||||
for (let i = 0; i < period; i++) {
|
||||
sum += data[i].close;
|
||||
}
|
||||
let ema = sum / period;
|
||||
|
||||
result.push({
|
||||
time: data[period - 1].time,
|
||||
value: parseFloat(ema.toFixed(2)),
|
||||
});
|
||||
|
||||
// Calculate EMA for the rest
|
||||
for (let i = period; i < data.length; i++) {
|
||||
ema = (data[i].close - ema) * multiplier + ema;
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
value: parseFloat(ema.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Bollinger Bands
|
||||
export function calculateBollingerBands(data: CandlestickData[], period: number = 20, stdDev: number = 2) {
|
||||
const sma = calculateSMA(data, period);
|
||||
const upper: LineData[] = [];
|
||||
const lower: LineData[] = [];
|
||||
|
||||
for (let i = period - 1; i < data.length; i++) {
|
||||
let sumSquaredDiff = 0;
|
||||
const smaValue = sma[i - (period - 1)].value;
|
||||
|
||||
for (let j = 0; j < period; j++) {
|
||||
const diff = data[i - j].close - smaValue;
|
||||
sumSquaredDiff += diff * diff;
|
||||
}
|
||||
|
||||
const standardDeviation = Math.sqrt(sumSquaredDiff / period);
|
||||
const upperBand = smaValue + (standardDeviation * stdDev);
|
||||
const lowerBand = smaValue - (standardDeviation * stdDev);
|
||||
|
||||
upper.push({
|
||||
time: data[i].time,
|
||||
value: parseFloat(upperBand.toFixed(2)),
|
||||
});
|
||||
|
||||
lower.push({
|
||||
time: data[i].time,
|
||||
value: parseFloat(lowerBand.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
return { upper, middle: sma, lower };
|
||||
}
|
||||
|
||||
// Relative Strength Index (RSI)
|
||||
export function calculateRSI(data: CandlestickData[], period: number = 14): LineData[] {
|
||||
const result: LineData[] = [];
|
||||
|
||||
if (data.length < period + 1) return result;
|
||||
|
||||
// Calculate initial average gain/loss
|
||||
let avgGain = 0;
|
||||
let avgLoss = 0;
|
||||
|
||||
for (let i = 1; i <= period; i++) {
|
||||
const change = data[i].close - data[i - 1].close;
|
||||
if (change > 0) {
|
||||
avgGain += change;
|
||||
} else {
|
||||
avgLoss += Math.abs(change);
|
||||
}
|
||||
}
|
||||
|
||||
avgGain /= period;
|
||||
avgLoss /= period;
|
||||
|
||||
// Calculate RSI for each period
|
||||
for (let i = period; i < data.length; i++) {
|
||||
const change = data[i].close - data[i - 1].close;
|
||||
const gain = change > 0 ? change : 0;
|
||||
const loss = change < 0 ? Math.abs(change) : 0;
|
||||
|
||||
avgGain = (avgGain * (period - 1) + gain) / period;
|
||||
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
||||
|
||||
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
||||
const rsi = 100 - (100 / (1 + rs));
|
||||
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
value: parseFloat(rsi.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// MACD (Moving Average Convergence Divergence)
|
||||
export function calculateMACD(
|
||||
data: CandlestickData[],
|
||||
fastPeriod: number = 12,
|
||||
slowPeriod: number = 26,
|
||||
signalPeriod: number = 9
|
||||
) {
|
||||
const emaFast = calculateEMA(data, fastPeriod);
|
||||
const emaSlow = calculateEMA(data, slowPeriod);
|
||||
|
||||
const macdLine: LineData[] = [];
|
||||
const macdData: number[] = [];
|
||||
|
||||
// Calculate MACD line
|
||||
for (let i = 0; i < Math.min(emaFast.length, emaSlow.length); i++) {
|
||||
const fastIdx = emaFast.findIndex(d => d.time === emaSlow[i].time);
|
||||
if (fastIdx !== -1) {
|
||||
const macdValue = emaFast[fastIdx].value - emaSlow[i].value;
|
||||
macdLine.push({
|
||||
time: emaSlow[i].time,
|
||||
value: parseFloat(macdValue.toFixed(2)),
|
||||
});
|
||||
macdData.push(macdValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate signal line (EMA of MACD)
|
||||
const signalLine: LineData[] = [];
|
||||
if (macdData.length >= signalPeriod) {
|
||||
const multiplier = 2 / (signalPeriod + 1);
|
||||
let ema = macdData.slice(0, signalPeriod).reduce((a, b) => a + b, 0) / signalPeriod;
|
||||
|
||||
signalLine.push({
|
||||
time: macdLine[signalPeriod - 1].time,
|
||||
value: parseFloat(ema.toFixed(2)),
|
||||
});
|
||||
|
||||
for (let i = signalPeriod; i < macdData.length; i++) {
|
||||
ema = (macdData[i] - ema) * multiplier + ema;
|
||||
signalLine.push({
|
||||
time: macdLine[i].time,
|
||||
value: parseFloat(ema.toFixed(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate histogram
|
||||
const histogram: LineData[] = [];
|
||||
for (let i = 0; i < signalLine.length; i++) {
|
||||
const macdIdx = macdLine.findIndex(d => d.time === signalLine[i].time);
|
||||
if (macdIdx !== -1) {
|
||||
histogram.push({
|
||||
time: signalLine[i].time,
|
||||
value: parseFloat((macdLine[macdIdx].value - signalLine[i].value).toFixed(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { macdLine, signalLine, histogram };
|
||||
}
|
||||
|
||||
// Volume Weighted Average Price (VWAP)
|
||||
export function calculateVWAP(data: CandlestickData[]): LineData[] {
|
||||
const result: LineData[] = [];
|
||||
let cumulativeTPV = 0; // Cumulative Typical Price × Volume
|
||||
let cumulativeVolume = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const typicalPrice = (data[i].high + data[i].low + data[i].close) / 3;
|
||||
const volume = data[i].volume || 0;
|
||||
|
||||
cumulativeTPV += typicalPrice * volume;
|
||||
cumulativeVolume += volume;
|
||||
|
||||
if (cumulativeVolume > 0) {
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
value: parseFloat((cumulativeTPV / cumulativeVolume).toFixed(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Stochastic Oscillator
|
||||
export function calculateStochastic(data: CandlestickData[], period: number = 14, smoothK: number = 3, smoothD: number = 3) {
|
||||
const kValues: LineData[] = [];
|
||||
const dValues: LineData[] = [];
|
||||
|
||||
// Calculate %K
|
||||
for (let i = period - 1; i < data.length; i++) {
|
||||
let lowestLow = data[i].low;
|
||||
let highestHigh = data[i].high;
|
||||
|
||||
for (let j = 1; j < period; j++) {
|
||||
lowestLow = Math.min(lowestLow, data[i - j].low);
|
||||
highestHigh = Math.max(highestHigh, data[i - j].high);
|
||||
}
|
||||
|
||||
const k = ((data[i].close - lowestLow) / (highestHigh - lowestLow)) * 100;
|
||||
kValues.push({
|
||||
time: data[i].time,
|
||||
value: parseFloat(k.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
// Smooth %K if needed
|
||||
const smoothedK = smoothK > 1 ? calculateSMAFromLineData(kValues, smoothK) : kValues;
|
||||
|
||||
// Calculate %D (SMA of %K)
|
||||
const smoothedD = calculateSMAFromLineData(smoothedK, smoothD);
|
||||
|
||||
return { k: smoothedK, d: smoothedD };
|
||||
}
|
||||
|
||||
// Helper function to calculate SMA from LineData
|
||||
function calculateSMAFromLineData(data: LineData[], period: number): LineData[] {
|
||||
const result: LineData[] = [];
|
||||
|
||||
for (let i = period - 1; i < data.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sum += data[i - j].value;
|
||||
}
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
value: parseFloat((sum / period).toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ import {
|
|||
HomeIcon,
|
||||
PresentationChartLineIcon,
|
||||
ServerStackIcon,
|
||||
BeakerIcon,
|
||||
CurrencyDollarIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
export interface NavigationItem {
|
||||
|
|
@ -19,10 +21,18 @@ export interface NavigationItem {
|
|||
|
||||
export const navigation: NavigationItem[] = [
|
||||
{ name: 'Dashboard', href: '/dashboard', icon: HomeIcon },
|
||||
{
|
||||
name: 'Market Data',
|
||||
icon: CurrencyDollarIcon,
|
||||
children: [
|
||||
{ name: 'Charts', href: '/charts', icon: ChartBarIcon },
|
||||
{ name: 'Exchanges', href: '/exchanges', icon: BuildingLibraryIcon },
|
||||
],
|
||||
},
|
||||
{ name: 'Portfolio', href: '/portfolio', icon: ChartBarIcon },
|
||||
{ name: 'Strategies', href: '/strategies', icon: DocumentTextIcon },
|
||||
{ name: 'Analytics', href: '/analytics', icon: PresentationChartLineIcon },
|
||||
{ name: 'Backtest', href: '/backtest', icon: BeakerIcon },
|
||||
{
|
||||
name: 'System',
|
||||
icon: ServerStackIcon,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
BeakerIcon,
|
||||
BuildingOfficeIcon,
|
||||
ChartBarIcon,
|
||||
CogIcon,
|
||||
|
|
@ -27,6 +28,12 @@ export const navigation = [
|
|||
icon: ChartBarIcon,
|
||||
current: false,
|
||||
},
|
||||
{
|
||||
name: 'Backtest',
|
||||
href: '/backtest',
|
||||
icon: BeakerIcon,
|
||||
current: false,
|
||||
},
|
||||
{
|
||||
name: 'Exchanges',
|
||||
href: '/exchanges',
|
||||
|
|
|
|||
189
bun.lock
189
bun.lock
|
|
@ -58,6 +58,14 @@
|
|||
"typescript": "^5.3.3",
|
||||
},
|
||||
},
|
||||
"apps/stock/core": {
|
||||
"name": "@stock-bot/core",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.16.3",
|
||||
"cargo-cp-artifact": "^0.1",
|
||||
},
|
||||
},
|
||||
"apps/stock/data-ingestion": {
|
||||
"name": "@stock-bot/data-ingestion",
|
||||
"version": "1.0.0",
|
||||
|
|
@ -103,13 +111,28 @@
|
|||
"typescript": "^5.0.0",
|
||||
},
|
||||
},
|
||||
"apps/stock/trading-engine": {
|
||||
"name": "@stock-bot/trading-engine",
|
||||
"apps/stock/orchestrator": {
|
||||
"name": "@stock-bot/orchestrator",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@stock-bot/cache": "*",
|
||||
"@stock-bot/config": "*",
|
||||
"@stock-bot/di": "*",
|
||||
"@stock-bot/logger": "*",
|
||||
"@stock-bot/questdb": "*",
|
||||
"@stock-bot/queue": "*",
|
||||
"@stock-bot/shutdown": "*",
|
||||
"@stock-bot/utils": "*",
|
||||
"axios": "^1.6.0",
|
||||
"hono": "^4.0.0",
|
||||
"socket.io": "^4.7.2",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"uuid": "^9.0.0",
|
||||
"zod": "^3.22.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0",
|
||||
"@types/node": "^20.11.0",
|
||||
"bun-types": "latest",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
},
|
||||
},
|
||||
"apps/stock/web-api": {
|
||||
|
|
@ -136,13 +159,19 @@
|
|||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.3.1",
|
||||
"lightweight-charts": "^4.1.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"react-virtuoso": "^4.12.8",
|
||||
"recharts": "^2.10.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^3.22.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
|
|
@ -525,6 +554,8 @@
|
|||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
|
||||
|
|
@ -621,6 +652,8 @@
|
|||
|
||||
"@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
|
||||
|
||||
"@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
|
|
@ -843,12 +876,16 @@
|
|||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="],
|
||||
|
||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||
|
||||
"@stock-bot/browser": ["@stock-bot/browser@workspace:libs/services/browser"],
|
||||
|
||||
"@stock-bot/cache": ["@stock-bot/cache@workspace:libs/core/cache"],
|
||||
|
||||
"@stock-bot/config": ["@stock-bot/config@workspace:libs/core/config"],
|
||||
|
||||
"@stock-bot/core": ["@stock-bot/core@workspace:apps/stock/core"],
|
||||
|
||||
"@stock-bot/coverage-cli": ["@stock-bot/coverage-cli@workspace:tools/coverage-cli"],
|
||||
|
||||
"@stock-bot/data-ingestion": ["@stock-bot/data-ingestion@workspace:apps/stock/data-ingestion"],
|
||||
|
|
@ -867,6 +904,8 @@
|
|||
|
||||
"@stock-bot/mongodb": ["@stock-bot/mongodb@workspace:libs/data/mongodb"],
|
||||
|
||||
"@stock-bot/orchestrator": ["@stock-bot/orchestrator@workspace:apps/stock/orchestrator"],
|
||||
|
||||
"@stock-bot/postgres": ["@stock-bot/postgres@workspace:libs/data/postgres"],
|
||||
|
||||
"@stock-bot/proxy": ["@stock-bot/proxy@workspace:libs/services/proxy"],
|
||||
|
|
@ -881,8 +920,6 @@
|
|||
|
||||
"@stock-bot/stock-config": ["@stock-bot/stock-config@workspace:apps/stock/config"],
|
||||
|
||||
"@stock-bot/trading-engine": ["@stock-bot/trading-engine@workspace:apps/stock/trading-engine"],
|
||||
|
||||
"@stock-bot/types": ["@stock-bot/types@workspace:libs/core/types"],
|
||||
|
||||
"@stock-bot/utils": ["@stock-bot/utils@workspace:libs/utils"],
|
||||
|
|
@ -921,6 +958,26 @@
|
|||
|
||||
"@types/cookiejar": ["@types/cookiejar@2.1.5", "", {}, "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q=="],
|
||||
|
||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="],
|
||||
|
||||
"@types/dockerode": ["@types/dockerode@3.3.41", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-5kOi6bcnEjqfJ68ZNV/bBvSMLNIucc0XbRmBO4hg5OoFCoP99eSRcbMysjkzV7ZxQEmmc/zMnv4A7odwuKFzDA=="],
|
||||
|
|
@ -995,7 +1052,7 @@
|
|||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
|
|
@ -1069,6 +1126,8 @@
|
|||
|
||||
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
|
||||
|
||||
"axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="],
|
||||
|
||||
"b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
|
@ -1085,6 +1144,8 @@
|
|||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
||||
|
||||
"basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="],
|
||||
|
||||
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||
|
|
@ -1147,6 +1208,8 @@
|
|||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001724", "", {}, "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA=="],
|
||||
|
||||
"cargo-cp-artifact": ["cargo-cp-artifact@0.1.9", "", { "bin": { "cargo-cp-artifact": "bin/cargo-cp-artifact.js" } }, "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA=="],
|
||||
|
||||
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
|
@ -1189,7 +1252,7 @@
|
|||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
|
|
@ -1213,6 +1276,28 @@
|
|||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
|
@ -1221,10 +1306,14 @@
|
|||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
|
||||
|
||||
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||
|
|
@ -1273,6 +1362,8 @@
|
|||
|
||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
|
@ -1287,6 +1378,12 @@
|
|||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"engine.io": ["engine.io@6.6.4", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g=="],
|
||||
|
||||
"engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="],
|
||||
|
||||
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
|
@ -1371,10 +1468,14 @@
|
|||
|
||||
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
||||
|
||||
"fancy-canvas": ["fancy-canvas@2.1.0", "", {}, "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ=="],
|
||||
|
||||
"fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="],
|
||||
|
||||
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
|
@ -1539,6 +1640,8 @@
|
|||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="],
|
||||
|
||||
"ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="],
|
||||
|
|
@ -1665,6 +1768,8 @@
|
|||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightweight-charts": ["lightweight-charts@4.2.3", "", { "dependencies": { "fancy-canvas": "2.1.0" } }, "sha512-5kS/2hY3wNYNzhnS8Gb+GAS07DX8GPF2YVDnd2NMC85gJVQ6RLU6YrXNgNJ6eg0AnWPwCnvaGtYmGky3HiLQEw=="],
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
|
@ -1779,7 +1884,7 @@
|
|||
|
||||
"nearley": ["nearley@2.20.1", "", { "dependencies": { "commander": "^2.19.0", "moo": "^0.5.0", "railroad-diagrams": "^1.0.0", "randexp": "0.4.6" }, "bin": { "nearleyc": "bin/nearleyc.js", "nearley-test": "bin/nearley-test.js", "nearley-unparse": "bin/nearley-unparse.js", "nearley-railroad": "bin/nearley-railroad.js" } }, "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
|
|
@ -1991,6 +2096,8 @@
|
|||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
|
@ -2019,7 +2126,9 @@
|
|||
|
||||
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
"react-hook-form": ["react-hook-form@7.59.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-kmkek2/8grqarTJExFNjy+RXDIP8yM+QTl3QL6m6Q8b2bih4ltmiXxH7T9n+yXNK477xPh5yZT/6vD8sYGzJTA=="],
|
||||
|
||||
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
|
|
@ -2027,6 +2136,10 @@
|
|||
|
||||
"react-router-dom": ["react-router-dom@7.6.2", "", { "dependencies": { "react-router": "7.6.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA=="],
|
||||
|
||||
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
|
||||
|
||||
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
|
||||
|
||||
"react-virtuoso": ["react-virtuoso@4.13.0", "", { "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", "react-dom": ">=16 || >=17 || >= 18 || >=19" } }, "sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA=="],
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
|
|
@ -2039,6 +2152,10 @@
|
|||
|
||||
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||
|
||||
"recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
|
||||
|
||||
"recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
|
@ -2141,6 +2258,14 @@
|
|||
|
||||
"smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="],
|
||||
|
||||
"socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="],
|
||||
|
||||
"socket.io-adapter": ["socket.io-adapter@2.5.5", "", { "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" } }, "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg=="],
|
||||
|
||||
"socket.io-client": ["socket.io-client@4.8.1", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ=="],
|
||||
|
||||
"socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="],
|
||||
|
||||
"socks": ["socks@2.8.5", "", { "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww=="],
|
||||
|
||||
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
||||
|
|
@ -2237,6 +2362,8 @@
|
|||
|
||||
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"titleize": ["titleize@3.0.0", "", {}, "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ=="],
|
||||
|
||||
"tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="],
|
||||
|
|
@ -2317,6 +2444,8 @@
|
|||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
|
||||
|
||||
"vite": ["vite@4.5.14", "", { "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", "rollup": "^3.27.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g=="],
|
||||
|
||||
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
|
||||
|
|
@ -2349,6 +2478,10 @@
|
|||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||
|
||||
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
|
@ -2435,6 +2568,8 @@
|
|||
|
||||
"@stock-bot/web-app/eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
|
||||
|
||||
"@types/cors/@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="],
|
||||
|
||||
"@types/docker-modem/@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="],
|
||||
|
||||
"@types/dockerode/@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="],
|
||||
|
|
@ -2451,8 +2586,6 @@
|
|||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
|
@ -2477,6 +2610,12 @@
|
|||
|
||||
"dockerode/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
||||
|
||||
"engine.io/@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="],
|
||||
|
||||
"engine.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
|
@ -2505,7 +2644,7 @@
|
|||
|
||||
"execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
"express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
|
|
@ -2563,22 +2702,36 @@
|
|||
|
||||
"prebuild-install/tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
|
||||
|
||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"protobufjs/@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="],
|
||||
|
||||
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||
|
||||
"react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||
|
||||
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"recharts/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||
|
||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"run-applescript/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"socket.io-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"table/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
|
@ -2609,8 +2762,6 @@
|
|||
|
||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"@mongodb-js/oidc-plugin/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"@mongodb-js/oidc-plugin/express/body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
||||
|
||||
"@mongodb-js/oidc-plugin/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||
|
|
@ -2793,10 +2944,10 @@
|
|||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"dockerode/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
|
@ -2849,8 +3000,6 @@
|
|||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@mongodb-js/oidc-plugin/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"@mongodb-js/oidc-plugin/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"@mongodb-js/oidc-plugin/express/body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@
|
|||
"apps/stock/web-app",
|
||||
"apps/stock/core",
|
||||
"apps/stock/orchestrator",
|
||||
"apps/stock/analytics",
|
||||
"tools/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue