diff --git a/apps/stock/web-app/package.json b/apps/stock/web-app/package.json index 3a8b914..57a237e 100644 --- a/apps/stock/web-app/package.json +++ b/apps/stock/web-app/package.json @@ -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", diff --git a/apps/stock/web-app/src/App.tsx b/apps/stock/web-app/src/App.tsx.old similarity index 100% rename from apps/stock/web-app/src/App.tsx rename to apps/stock/web-app/src/App.tsx.old diff --git a/apps/stock/web-app/src/app/App.tsx b/apps/stock/web-app/src/app/App.tsx index aed1130..256a540 100644 --- a/apps/stock/web-app/src/app/App.tsx +++ b/apps/stock/web-app/src/app/App.tsx @@ -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() { }> } /> } /> + } /> } /> Analytics Page - Coming Soon} /> + } /> Settings Page - Coming Soon} /> } /> } /> diff --git a/apps/stock/web-app/src/features/backtest/BacktestPage.tsx b/apps/stock/web-app/src/features/backtest/BacktestPage.tsx new file mode 100644 index 0000000..786fcf2 --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/BacktestPage.tsx @@ -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 ( +
+
+

Backtest Strategy

+

+ Test your trading strategies against historical data to evaluate performance and risk. +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + + {config && ( + + )} +
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx b/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx new file mode 100644 index 0000000..474a3e0 --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx @@ -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({ + 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) => { + 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 ( +
+

Configuration

+ +
+
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ 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} + /> + +
+
+ {formData.symbols.map(symbol => ( + + {symbol} + + + ))} +
+
+ +
+ + +
+ +
+
+ + 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} + /> +
+ +
+ + 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} + /> +
+
+ +
+ + +

1x = real-time, 10x = 10x faster

+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/components/BacktestControls.tsx b/apps/stock/web-app/src/features/backtest/components/BacktestControls.tsx new file mode 100644 index 0000000..8b41b9b --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/components/BacktestControls.tsx @@ -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 ( +
+

Controls

+ +
+
+ {status === 'configured' || status === 'stopped' ? ( + + ) : status === 'running' ? ( + + ) : status === 'paused' ? ( + + ) : null} + + {(status === 'running' || status === 'paused') && ( + + )} + + {status === 'paused' && ( + + )} +
+ + {status !== 'idle' && status !== 'configured' && ( + <> +
+
+ Progress + {progress.toFixed(1)}% +
+
+
+
+
+ +
+
+ Status + + {status.charAt(0).toUpperCase() + status.slice(1)} + +
+ + {currentTime && ( +
+ Current Time + + {formatTime(currentTime)} + +
+ )} +
+ + )} + + {status === 'completed' && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx b/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx new file mode 100644 index 0000000..cdb3729 --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx @@ -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 ( +
+
+

+ Configure Your Backtest +

+

+ Set up your strategy parameters and click "Configure Backtest" to begin. +

+
+
+ ); + } + + if (status === 'configured') { + return ( +
+
+

+ Ready to Start +

+

+ Click the "Start" button to begin backtesting your strategy. +

+
+
+ ); + } + + if (status === 'running' && !results) { + return ( +
+
+
+

+ Running Backtest... +

+

+ Processing historical data and executing trades. +

+
+
+ ); + } + + if (!results) { + return ( +
+
+

+ No Results Yet +

+

+ Results will appear here once the backtest is complete. +

+
+
+ ); + } + + return ( +
+ {/* Metrics Overview */} +
+ = 0 ? '+' : ''}${results.metrics.totalReturn.toFixed(2)}%`} + trend={results.metrics.totalReturn >= 0 ? 'up' : 'down'} + /> + = 1 ? 'up' : 'down'} + /> + + = 50 ? 'up' : 'down'} + /> + + +
+ + {/* Performance Chart Placeholder */} +
+

+ Portfolio Performance +

+
+

+ Performance chart will be displayed here (requires recharts) +

+
+
+ + {/* Positions Table */} + {results.positions.length > 0 && ( +
+

+ Current Positions +

+ +
+ )} + + {/* Trade Log */} + {results.trades.length > 0 && ( +
+

+ Trade History +

+ +
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/components/MetricsCard.tsx b/apps/stock/web-app/src/features/backtest/components/MetricsCard.tsx new file mode 100644 index 0000000..e9883ba --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/components/MetricsCard.tsx @@ -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 ( +
+

{title}

+
+

+ {value} +

+ {trend && ( + + {trend === 'up' ? + : + + } + + )} +
+ {subtitle && ( +

{subtitle}

+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/components/PositionsTable.tsx b/apps/stock/web-app/src/features/backtest/components/PositionsTable.tsx new file mode 100644 index 0000000..43671ed --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/components/PositionsTable.tsx @@ -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 ( +
+ + + + + + + + + + + + + {positions.map((position) => { + const totalPnl = position.realizedPnl + position.unrealizedPnl; + return ( + + + + + + + + + ); + })} + +
SymbolQuantityAvg PriceCurrentP&LUnrealized
{position.symbol} + {position.quantity.toLocaleString()} + + {formatCurrency(position.averagePrice)} + + {formatCurrency(position.currentPrice)} + = 0 ? 'text-success' : 'text-error' + }`}> + {formatPnl(totalPnl)} + = 0 ? 'text-success' : 'text-error' + }`}> + {formatPnl(position.unrealizedPnl)} +
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/components/TradeLog.tsx b/apps/stock/web-app/src/features/backtest/components/TradeLog.tsx new file mode 100644 index 0000000..2925df8 --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/components/TradeLog.tsx @@ -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 ( +
+ + + + + + + + + + + {trades.some(t => t.pnl !== undefined) && ( + + )} + + + + {sortedTrades.map((trade) => { + const tradeValue = trade.quantity * trade.price; + return ( + + + + + + + + + {trade.pnl !== undefined && ( + + )} + + ); + })} + +
TimeSymbolSideQuantityPriceValueComm.P&L
+ {formatTime(trade.timestamp)} + {trade.symbol} + + {trade.side.toUpperCase()} + + + {trade.quantity.toLocaleString()} + + {formatCurrency(trade.price)} + + {formatCurrency(tradeValue)} + + {formatCurrency(trade.commission)} + = 0 ? 'text-success' : 'text-error' + }`}> + {trade.pnl >= 0 ? '+' : ''}{formatCurrency(trade.pnl)} +
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/components/index.ts b/apps/stock/web-app/src/features/backtest/components/index.ts new file mode 100644 index 0000000..ba4f5b0 --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/components/index.ts @@ -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'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/hooks/index.ts b/apps/stock/web-app/src/features/backtest/hooks/index.ts new file mode 100644 index 0000000..f0a2e7c --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/hooks/index.ts @@ -0,0 +1 @@ +export { useBacktest } from './useBacktest'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts b/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts new file mode 100644 index 0000000..f567447 --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts @@ -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(null); + const [config, setConfig] = useState(null); + const [status, setStatus] = useState('idle'); + const [results, setResults] = useState(null); + const [currentTime, setCurrentTime] = useState(null); + const [progress, setProgress] = useState(0); + const [error, setError] = useState(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, + }; +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/index.ts b/apps/stock/web-app/src/features/backtest/index.ts new file mode 100644 index 0000000..caceadc --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/index.ts @@ -0,0 +1,2 @@ +export { BacktestPage } from './BacktestPage'; +export * from './types'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/services/backtestService.ts b/apps/stock/web-app/src/features/backtest/services/backtestService.ts new file mode 100644 index 0000000..72152de --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/services/backtestService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + }; + } +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/types/backtest.types.ts b/apps/stock/web-app/src/features/backtest/types/backtest.types.ts new file mode 100644 index 0000000..bf815da --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/types/backtest.types.ts @@ -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; +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/types/index.ts b/apps/stock/web-app/src/features/backtest/types/index.ts new file mode 100644 index 0000000..371ae05 --- /dev/null +++ b/apps/stock/web-app/src/features/backtest/types/index.ts @@ -0,0 +1,12 @@ +export type { + BacktestStatus, + BacktestConfig, + BacktestMetrics, + Position, + Trade, + PerformanceDataPoint, + BacktestResult, + OrderBookLevel, + OrderBookSnapshot, + RiskMetrics, +} from './backtest.types'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/ChartPage.tsx b/apps/stock/web-app/src/features/charts/ChartPage.tsx new file mode 100644 index 0000000..b60dd4f --- /dev/null +++ b/apps/stock/web-app/src/features/charts/ChartPage.tsx @@ -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({ + symbol: 'AAPL', + interval: '1d', + chartType: 'candlestick', + showVolume: true, + indicators: [], + theme: 'dark', + }); + + const [chartData, setChartData] = useState([]); + const [marketData, setMarketData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [showIndicatorSelector, setShowIndicatorSelector] = useState(false); + const [showIndicatorSettings, setShowIndicatorSettings] = useState(false); + const [selectedIndicator, setSelectedIndicator] = useState(null); + const [activeIndicators, setActiveIndicators] = useState([ + { + 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) => { + 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 = { + 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 ( +
+
+

Market Charts

+

+ Real-time market data and advanced charting powered by TradingView. +

+
+ +
+ {/* Left Sidebar - Indicator List */} + {showSidebar && ( + ({ + 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 */} +
+ + +
+ {isLoading ? ( +
+
+
+

Loading chart data...

+
+
+ ) : ( + 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" + /> + )} +
+
+
+ + {/* Indicator Selector Modal */} + setShowIndicatorSelector(false)} + selectedIndicators={activeIndicators.map(i => i.id)} + onIndicatorsChange={handleAddIndicators} + /> + + {/* Indicator Settings Modal */} + setShowIndicatorSettings(false)} + indicator={selectedIndicator} + onSave={handleSaveIndicatorSettings} + /> +
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/ChartErrorBoundary.tsx b/apps/stock/web-app/src/features/charts/components/ChartErrorBoundary.tsx new file mode 100644 index 0000000..c4df966 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/ChartErrorBoundary.tsx @@ -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 { + 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 ( +
+
+
⚠️
+

+ Chart Loading Error +

+

+ {this.state.error?.message || 'Unable to load the chart'} +

+ +
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/ChartOverlay.tsx b/apps/stock/web-app/src/features/charts/components/ChartOverlay.tsx new file mode 100644 index 0000000..4a5ffb5 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/ChartOverlay.tsx @@ -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 ( +
+ {/* Indicators */} + {(indicators.length > 0 || true) && ( +
+ {/* Indicators Header with Toggle */} +
+ Indicators + +
+ + {/* Indicator List */} + {showIndicators && ( +
+ {indicators.map((indicator) => ( +
+ {/* Eye icon on the left */} + {onIndicatorToggle && ( + + )} + + {/* Color dot */} +
+ + {/* Name and value */} +
+ + {indicator.name} + + {indicator.value !== undefined && indicator.visible !== false && ( + + {indicator.value.toFixed(2)} + + )} +
+ + {/* Spacer to push buttons to the right */} +
+ + {/* Settings and Remove on the right */} +
+ + {indicator.id !== 'VOLUME' && ( + + )} +
+
+ ))} + + {/* Add Indicator Button */} + {onAddIndicator && ( + + )} +
+ )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/ChartToolbar.tsx b/apps/stock/web-app/src/features/charts/components/ChartToolbar.tsx new file mode 100644 index 0000000..dafd63f --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/ChartToolbar.tsx @@ -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) => 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 ( +
+
+ {/* Symbol Input */} +
+ 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" + /> +
+ + {/* Interval Selector */} +
+ {intervals.map(({ value, label }) => ( + + ))} +
+ + {/* Chart Type Selector */} +
+ {chartTypes.map(({ value, label }) => ( + + ))} +
+
+ +
+ {/* Volume Toggle */} + + + + {/* Theme Toggle */} + +
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/IndicatorList.tsx b/apps/stock/web-app/src/features/charts/components/IndicatorList.tsx new file mode 100644 index 0000000..f7f78c9 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/IndicatorList.tsx @@ -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(null); + + // Group indicators by type + const overlayIndicators = indicators.filter(i => i.type === 'overlay'); + const panelIndicators = indicators.filter(i => i.type === 'panel'); + + return ( +
+ {/* Header */} +
+
+

Indicators

+ +
+
+ + {/* Indicator List */} +
+ {/* Main Chart Section */} +
+
Main Chart
+ + {/* Price Action */} +
+
+
+
+ Price +
+ +
+
+ + {/* Volume */} +
+
+
+
+ Volume +
+
+ + +
+
+
+ + {/* Overlay Indicators */} + {overlayIndicators.map(indicator => ( +
setHoveredId(indicator.id)} + onMouseLeave={() => setHoveredId(null)} + > +
+
+
+ {indicator.name} +
+
+ + + +
+
+
+ ))} +
+ + {/* Panel Indicators */} + {panelIndicators.length > 0 && ( +
+
Separate Panels
+ {panelIndicators.map(indicator => ( +
setHoveredId(indicator.id)} + onMouseLeave={() => setHoveredId(null)} + > +
+
+
+ {indicator.name} +
+
+ + + +
+
+
+ ))} +
+ )} +
+ + {/* Footer */} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/IndicatorSelector.tsx b/apps/stock/web-app/src/features/charts/components/IndicatorSelector.tsx new file mode 100644 index 0000000..0d7668a --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/IndicatorSelector.tsx @@ -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>(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 ( + + + +
+ + +
+
+ + +
+
+ + Technical Indicators + + +
+ +
+ {/* Overlay Indicators */} +
+

Overlay Indicators

+
+ {overlayIndicators.map(indicator => ( + + ))} +
+
+ + {/* Oscillator Indicators */} +
+

Oscillators (Coming Soon)

+
+ {oscillatorIndicators.map(indicator => ( + + ))} +
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/IndicatorSettings.tsx b/apps/stock/web-app/src/features/charts/components/IndicatorSettings.tsx new file mode 100644 index 0000000..20bc894 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/IndicatorSettings.tsx @@ -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; + 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> = { + 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> = { + 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(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 ( + + + +
+ + +
+
+ + +
+
+ + {config.name} Settings + + +
+ +
+ {/* Parameters */} + {parameters.length > 0 && ( +
+

Parameters

+
+ {parameters.map(param => ( +
+ + {param.type === 'number' ? ( + 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" + /> + ) : ( + + )} +
+ ))} +
+
+ )} + + {/* Style */} +
+

Style

+
+
+ +
+ handleStyleChange('color', e.target.value)} + className="w-10 h-10 rounded border border-border cursor-pointer" + /> + 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" + /> +
+
+ +
+ + handleStyleChange('lineWidth', parseInt(e.target.value))} + className="w-full" + /> +
+ {config.style.lineWidth || 2}px +
+
+ +
+ + +
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/MarketOverview.tsx b/apps/stock/web-app/src/features/charts/components/MarketOverview.tsx new file mode 100644 index 0000000..c0ebb18 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/MarketOverview.tsx @@ -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 ( +
+
+
+
+
+
+ ); + } + + const isPositive = data.change >= 0; + + return ( +
+
+
+
+

{data.symbol}

+ {data.name} +
+
+ + ${data.price.toFixed(2)} + +
+ {isPositive ? ( + + ) : ( + + )} + + ${Math.abs(data.change).toFixed(2)} + + + ({isPositive ? '+' : ''}{data.changePercent.toFixed(2)}%) + +
+
+
+ +
+
+ Open +

${data.open.toFixed(2)}

+
+
+ High +

${data.high.toFixed(2)}

+
+
+ Low +

${data.low.toFixed(2)}

+
+
+ Prev Close +

${data.previousClose.toFixed(2)}

+
+
+ Volume +

+ {(data.volume / 1000000).toFixed(2)}M +

+
+
+ Time +

+ {new Date(data.timestamp).toLocaleTimeString()} +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/SymbolChart.tsx b/apps/stock/web-app/src/features/charts/components/SymbolChart.tsx new file mode 100644 index 0000000..f047f08 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/SymbolChart.tsx @@ -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; + 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(null); + const chartRef = useRef(null); + const seriesRef = useRef | null>(null); + const volumeSeriesRef = useRef | null>(null); + const indicatorSeriesRef = useRef>>(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>(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(); + 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 ( +
+ {isLoading && ( +
+
+
+ )} + + {/* Chart container */} +
+ + {/* Chart Overlay with indicators */} + {!isLoading && onIndicatorSettings && onIndicatorRemove && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/components/index.ts b/apps/stock/web-app/src/features/charts/components/index.ts new file mode 100644 index 0000000..09e81a0 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/components/index.ts @@ -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'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/hooks/index.ts b/apps/stock/web-app/src/features/charts/hooks/index.ts new file mode 100644 index 0000000..2ce0ad5 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/hooks/index.ts @@ -0,0 +1 @@ +export { useChartData } from './useChartData'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/hooks/useChartData.ts b/apps/stock/web-app/src/features/charts/hooks/useChartData.ts new file mode 100644 index 0000000..68dcf1e --- /dev/null +++ b/apps/stock/web-app/src/features/charts/hooks/useChartData.ts @@ -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([]); + const [marketData, setMarketData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(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, + }; +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/index.ts b/apps/stock/web-app/src/features/charts/index.ts new file mode 100644 index 0000000..d873ad1 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/index.ts @@ -0,0 +1,3 @@ +export { ChartPage } from './ChartPage'; +export * from './types'; +export * from './components'; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/services/chartDataService.ts b/apps/stock/web-app/src/features/charts/services/chartDataService.ts new file mode 100644 index 0000000..48f80a2 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/services/chartDataService.ts @@ -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 { + 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 { + 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> { + 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 = { + '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 }; + } +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/types/index.ts b/apps/stock/web-app/src/features/charts/types/index.ts new file mode 100644 index 0000000..8f008ae --- /dev/null +++ b/apps/stock/web-app/src/features/charts/types/index.ts @@ -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; + 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; + }; +} \ No newline at end of file diff --git a/apps/stock/web-app/src/features/charts/utils/indicators.ts b/apps/stock/web-app/src/features/charts/utils/indicators.ts new file mode 100644 index 0000000..bf16f92 --- /dev/null +++ b/apps/stock/web-app/src/features/charts/utils/indicators.ts @@ -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; +} \ No newline at end of file diff --git a/apps/stock/web-app/src/lib/constants.ts b/apps/stock/web-app/src/lib/constants.ts index d6fc1d5..92d68ef 100644 --- a/apps/stock/web-app/src/lib/constants.ts +++ b/apps/stock/web-app/src/lib/constants.ts @@ -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: 'Exchanges', href: '/exchanges', icon: BuildingLibraryIcon }, + { + 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, diff --git a/apps/stock/web-app/src/lib/constants/navigation.ts b/apps/stock/web-app/src/lib/constants/navigation.ts index 1351bee..84474fc 100644 --- a/apps/stock/web-app/src/lib/constants/navigation.ts +++ b/apps/stock/web-app/src/lib/constants/navigation.ts @@ -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', diff --git a/bun.lock b/bun.lock index 6089540..fef587d 100644 --- a/bun.lock +++ b/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=="], diff --git a/package.json b/package.json index ff282aa..134e721 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "apps/stock/web-app", "apps/stock/core", "apps/stock/orchestrator", - "apps/stock/analytics", "tools/*" ], "devDependencies": {