work on clean up and switched all to use eodSearchCode

This commit is contained in:
Boki 2025-07-13 13:42:22 -04:00
parent d68268b722
commit e341cc0226
19 changed files with 206 additions and 127 deletions

View file

@ -1 +0,0 @@
../../../node_modules

View file

@ -130,11 +130,9 @@ export async function fetchCorporateActions(
} }
// Build URL based on action type // Build URL based on action type
// Use utility function to handle US symbols and EUFUND special case // eodSearchCode already contains the symbol with exchange suffix (e.g., AAPL.US)
const exchangeSuffix = getEodExchangeSuffix(exchange, country);
const endpoint = actionType === 'dividends' ? 'div' : 'splits'; const endpoint = actionType === 'dividends' ? 'div' : 'splits';
const url = new URL(`https://eodhd.com/api/${endpoint}/${symbol}.${exchangeSuffix}`); const url = new URL(`https://eodhd.com/api/${endpoint}/${eodSearchCode}`);
url.searchParams.append('api_token', apiKey); url.searchParams.append('api_token', apiKey);
url.searchParams.append('fmt', 'json'); url.searchParams.append('fmt', 'json');

View file

@ -139,18 +139,17 @@ export async function fetchBulkFundamentals(
throw new Error('EOD API key not configured'); throw new Error('EOD API key not configured');
} }
// Group symbols by actual exchange for API endpoint, but use country for symbol suffix // Group symbols by actual exchange for API endpoint
// eodSearchCode already contains the symbol with exchange suffix (e.g., AAPL.US)
const exchangeGroups = symbolDocs.reduce((acc, symbolDoc) => { const exchangeGroups = symbolDocs.reduce((acc, symbolDoc) => {
const symbol = symbolDoc.Code;
const exchange = symbolDoc.eodExchange || symbolDoc.Exchange; const exchange = symbolDoc.eodExchange || symbolDoc.Exchange;
const country = symbolDoc.Country; const eodSearchCode = symbolDoc.eodSearchCode;
if (!acc[exchange]) { if (!acc[exchange]) {
acc[exchange] = []; acc[exchange] = [];
} }
// Use utility function to handle US symbols and EUFUND special case // eodSearchCode already has the correct format (e.g., AAPL.US)
const exchangeSuffix = getEodExchangeSuffix(exchange, country); acc[exchange].push(eodSearchCode);
acc[exchange].push(`${symbol}.${exchangeSuffix}`);
return acc; return acc;
}, {} as Record<string, string[]>); }, {} as Record<string, string[]>);
@ -309,10 +308,8 @@ export async function fetchSingleFundamentals(
} }
// Build URL for single fundamentals endpoint // Build URL for single fundamentals endpoint
// Use utility function to handle US symbols and EUFUND special case // eodSearchCode already contains the symbol with exchange suffix (e.g., AAPL.US)
const exchangeSuffix = getEodExchangeSuffix(exchange, country); const url = new URL(`https://eodhd.com/api/fundamentals/${eodSearchCode}`);
const url = new URL(`https://eodhd.com/api/fundamentals/${symbol}.${exchangeSuffix}`);
url.searchParams.append('api_token', apiKey); url.searchParams.append('api_token', apiKey);
url.searchParams.append('fmt', 'json'); url.searchParams.append('fmt', 'json');

View file

@ -303,7 +303,7 @@ export async function crawlIntraday(
finished: updateData.finished finished: updateData.finished
}; };
} catch (error) { } catch (error) {
logger.error('Failed to crawl intraday data', { error, symbol, exchange, interval }); logger.error('Failed to crawl intraday data', { error, eodSearchCode, interval });
throw error; throw error;
} }
} }

View file

@ -39,7 +39,7 @@ import { createEODOperationRegistry } from './shared';
], ],
}) })
export class EodHandler extends BaseHandler<DataIngestionServices> { export class EodHandler extends BaseHandler<DataIngestionServices> {
public operationRegistry: OperationRegistry; public operationRegistry!: OperationRegistry;
constructor(services: any) { constructor(services: any) {
super(services); super(services);

View file

@ -35,7 +35,7 @@ import { createQMOperationRegistry } from './shared/operation-provider';
@Handler('qm') @Handler('qm')
@Disabled() // Disable by default, enable specific operations as needed @Disabled() // Disable by default, enable specific operations as needed
export class QMHandler extends BaseHandler<DataIngestionServices> { export class QMHandler extends BaseHandler<DataIngestionServices> {
public operationRegistry: OperationRegistry; public operationRegistry!: OperationRegistry;
constructor(services: any) { constructor(services: any) {
super(services); // Handler name read from @Handler decorator super(services); // Handler name read from @Handler decorator

View file

@ -7,11 +7,10 @@ import { BaseOperationProvider } from './BaseOperationProvider';
import { OperationTracker } from './OperationTracker'; import { OperationTracker } from './OperationTracker';
import type { import type {
OperationComponentOptions, OperationComponentOptions,
OperationUpdate, OperationConfig,
StaleSymbolOptions,
BulkOperationUpdate,
OperationStats, OperationStats,
OperationConfig OperationUpdate,
StaleSymbolOptions
} from './types'; } from './types';
/** /**

View file

@ -161,7 +161,20 @@ export class PerformanceAnalyzer {
sortinoRatio, sortinoRatio,
calmarRatio, calmarRatio,
informationRatio, informationRatio,
...tradeStats, totalTrades: tradeStats.totalTrades ?? 0,
winRate: tradeStats.winRate ?? 0,
avgWin: tradeStats.avgWin ?? 0,
avgLoss: tradeStats.avgLoss ?? 0,
avgWinLoss: tradeStats.avgWinLoss ?? 0,
profitFactor: tradeStats.profitFactor ?? 0,
expectancy: tradeStats.expectancy ?? 0,
payoffRatio: tradeStats.payoffRatio ?? 0,
avgHoldingPeriod: tradeStats.avgHoldingPeriod ?? 0,
avgTradesPerDay: tradeStats.avgTradesPerDay ?? 0,
maxConsecutiveWins: tradeStats.maxConsecutiveWins ?? 0,
maxConsecutiveLosses: tradeStats.maxConsecutiveLosses ?? 0,
largestWin: tradeStats.largestWin ?? 0,
largestLoss: tradeStats.largestLoss ?? 0,
skewness, skewness,
kurtosis, kurtosis,
tailRatio, tailRatio,
@ -178,9 +191,8 @@ export class PerformanceAnalyzer {
if (this.equityCurve.length === 0) { if (this.equityCurve.length === 0) {
return { return {
maxDrawdown: 0, maxDrawdown: 0,
averageDrawdown: 0,
maxDrawdownDuration: 0, maxDrawdownDuration: 0,
underwaterTime: 0, underwaterCurve: [],
drawdownPeriods: [], drawdownPeriods: [],
currentDrawdown: 0 currentDrawdown: 0
}; };

View file

@ -98,7 +98,7 @@ export class BacktestEngine extends EventEmitter {
private lastProcessTime = 0; private lastProcessTime = 0;
private dataManager: DataManager; private dataManager: DataManager;
private marketSimulator: MarketSimulator; private marketSimulator: MarketSimulator;
private performanceAnalyzer: PerformanceAnalyzer; private performanceAnalyzer!: PerformanceAnalyzer;
private microstructures: Map<string, MarketMicrostructure> = new Map(); private microstructures: Map<string, MarketMicrostructure> = new Map();
private container: IServiceContainer; private container: IServiceContainer;
private runId: string | null = null; private runId: string | null = null;

View file

@ -1,4 +1,4 @@
import React, { Component, ReactNode } from 'react'; import React, { Component, type ReactNode } from 'react';
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@ -20,11 +20,11 @@ export class ErrorBoundary extends Component<Props, State> {
return { hasError: true, error }; return { hasError: true, error };
} }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo); console.error('ErrorBoundary caught an error:', error, errorInfo);
} }
render() { override render() {
if (this.state.hasError) { if (this.state.hasError) {
return ( return (
this.props.fallback || ( this.props.fallback || (

View file

@ -58,7 +58,6 @@ export function Chart({
return `${chartId}-${Date.now()}`; return `${chartId}-${Date.now()}`;
}, [chartId]) }, [chartId])
const mountedRef = useRef(true); const mountedRef = useRef(true);
const lastChartIdRef = useRef(chartId);
// Track component mount state // Track component mount state
useEffect(() => { useEffect(() => {
@ -126,13 +125,13 @@ export function Chart({
// Use requestAnimationFrame to ensure DOM is ready and avoid conflicts // Use requestAnimationFrame to ensure DOM is ready and avoid conflicts
const rafId = requestAnimationFrame(() => { const rafId = requestAnimationFrame(() => {
const initTimeout = setTimeout(() => { setTimeout(() => {
if (!chartContainerRef.current || isCleanedUp || !mountedRef.current) return; if (!chartContainerRef.current || isCleanedUp || !mountedRef.current) return;
// Create chart using the chart manager // Create chart using the chart manager
const chart = createChart(uniqueChartId, chartContainerRef.current, { const chart = createChart(uniqueChartId, chartContainerRef.current, {
width: chartContainerRef.current.clientWidth, width: chartContainerRef.current.clientWidth,
height: height, height: typeof height === 'string' ? parseInt(height) : height,
layout: { layout: {
background: { background: {
type: LightweightCharts.ColorType.Solid, type: LightweightCharts.ColorType.Solid,
@ -188,7 +187,7 @@ export function Chart({
time: timeInSeconds as LightweightCharts.Time time: timeInSeconds as LightweightCharts.Time
}; };
}) })
.filter((item, index) => { .filter((item) => {
if (seen.has(item.time)) { if (seen.has(item.time)) {
return false; return false;
} }
@ -201,7 +200,7 @@ export function Chart({
// Create main series // Create main series
let mainSeries: LightweightCharts.ISeriesApi<any> | null = null; let mainSeries: LightweightCharts.ISeriesApi<any> | null = null;
if (type === 'candlestick' && data[0].open !== undefined) { if (type === 'candlestick' && data[0]?.open !== undefined) {
mainSeries = chart.addCandlestickSeries({ mainSeries = chart.addCandlestickSeries({
upColor: '#10b981', upColor: '#10b981',
downColor: '#ef4444', downColor: '#ef4444',
@ -212,7 +211,7 @@ export function Chart({
}); });
const validData = validateAndFilterData(data); const validData = validateAndFilterData(data);
mainSeries.setData(validData as LightweightCharts.CandlestickData[]); mainSeries.setData(validData as LightweightCharts.CandlestickData[]);
} else if (type === 'line' || (type === 'candlestick' && data[0].value !== undefined)) { } else if (type === 'line' || (type === 'candlestick' && data[0]?.value !== undefined)) {
mainSeries = chart.addLineSeries({ mainSeries = chart.addLineSeries({
color: '#3b82f6', color: '#3b82f6',
lineWidth: 2, lineWidth: 2,
@ -278,7 +277,7 @@ export function Chart({
overlayData.forEach((overlay, index) => { overlayData.forEach((overlay, index) => {
const series = chart.addLineSeries({ const series = chart.addLineSeries({
color: overlay.color || ['#ff9800', '#4caf50', '#9c27b0', '#f44336'][index % 4], color: overlay.color || ['#ff9800', '#4caf50', '#9c27b0', '#f44336'][index % 4],
lineWidth: overlay.lineWidth || 2, lineWidth: (overlay.lineWidth || 2) as LightweightCharts.LineWidth,
title: overlay.name, title: overlay.name,
priceScaleId: index === 0 ? '' : `overlay-${index}`, // First overlay uses main scale priceScaleId: index === 0 ? '' : `overlay-${index}`, // First overlay uses main scale
}); });
@ -371,7 +370,7 @@ export function Chart({
}); });
} catch (error) { } catch (error) {
// Ignore errors if the chart is being disposed // Ignore errors if the chart is being disposed
if (!error?.message?.includes('disposed')) { if (!(error instanceof Error) || !error.message?.includes('disposed')) {
console.error('Error resizing chart:', error); console.error('Error resizing chart:', error);
} }
} }

View file

@ -1,4 +1,4 @@
import { ReactNode } from 'react'; import type { ReactNode } from 'react';
export interface TableColumn<T = Record<string, unknown>> { export interface TableColumn<T = Record<string, unknown>> {
id: string; id: string;

View file

@ -38,13 +38,12 @@ export function BacktestDetailPageV2() {
createRun, createRun,
pauseRun, pauseRun,
resumeRun, resumeRun,
cancelRun,
updateRunSpeed, updateRunSpeed,
selectRun, selectRun,
} = useBacktestV2(); } = useBacktestV2();
// WebSocket connection for real-time updates // WebSocket connection for real-time updates
const { isConnected } = useWebSocket({ useWebSocket({
runId: currentRun?.id || null, runId: currentRun?.id || null,
onProgress: (progress, currentDate) => { onProgress: (progress, currentDate) => {
// Update the run progress in the UI // Update the run progress in the UI
@ -60,7 +59,7 @@ export function BacktestDetailPageV2() {
loadBacktest(id); loadBacktest(id);
} }
}, },
onCompleted: async (results) => { onCompleted: async () => {
// When run completes, reload to get the final results // When run completes, reload to get the final results
if (currentRun?.id) { if (currentRun?.id) {
try { try {

View file

@ -3,8 +3,36 @@ import { useState, useMemo, memo } from 'react';
import { Chart } from '../../../components/charts/Chart'; import { Chart } from '../../../components/charts/Chart';
import { ChartContainer } from './ChartContainer'; import { ChartContainer } from './ChartContainer';
interface ExtendedBacktestResult extends BacktestResult {
equity?: Array<{ date: string; value: number }>;
ohlcData?: Record<string, Array<{
timestamp: number;
open: number;
high: number;
low: number;
close: number;
volume?: number;
}>>;
runId?: string;
}
interface ExtendedTrade {
id: string;
timestamp: string;
symbol: string;
side: 'buy' | 'sell';
quantity: number;
price: number;
commission: number;
pnl?: number;
positionAfter: number;
entryPrice?: number;
entryDate?: string;
date?: string;
}
interface BacktestChartProps { interface BacktestChartProps {
result: BacktestResult | null; result: ExtendedBacktestResult | null;
isLoading: boolean; isLoading: boolean;
} }
@ -13,36 +41,43 @@ export const BacktestChart = memo(function BacktestChart({ result, isLoading }:
const [selectedSymbol, setSelectedSymbol] = useState<string>(''); const [selectedSymbol, setSelectedSymbol] = useState<string>('');
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!result?.equity || !result?.ohlcData) return null; if (!result || (!result.equity && !result.performanceData) || (!result.ohlcData && !result.trades)) return null;
const symbols = Object.keys(result.ohlcData); const symbols = result.ohlcData ? Object.keys(result.ohlcData) : (result.trades ? [...new Set(result.trades.map(t => t.symbol))] : []);
const symbol = selectedSymbol || symbols[0] || ''; const symbol = selectedSymbol || symbols[0] || '';
const ohlcData = result.ohlcData[symbol] || []; const ohlcData = result.ohlcData?.[symbol] || [];
// Remove excessive logging in production // Remove excessive logging in production
// Log only on significant changes // Log only on significant changes
if (process.env.NODE_ENV === 'development' && ohlcData.length > 0) { if (process.env.NODE_ENV === 'development' && ohlcData.length > 0) {
// Use a simple hash to detect actual data changes // Use a simple hash to detect actual data changes
const dataHash = `${symbols.length}-${result.equity?.length}-${ohlcData.length}`; const dataHash = `${symbols.length}-${result.equity?.length || result.performanceData?.length}-${ohlcData.length}`;
if ((window as any).__lastDataHash !== dataHash) { if ((window as any).__lastDataHash !== dataHash) {
(window as any).__lastDataHash = dataHash; (window as any).__lastDataHash = dataHash;
console.log('BacktestChart data updated:', { console.log('BacktestChart data updated:', {
symbols, symbols,
selectedSymbol, selectedSymbol,
symbol, symbol,
ohlcDataKeys: Object.keys(result.ohlcData), ohlcDataKeys: result.ohlcData ? Object.keys(result.ohlcData) : [],
equityLength: result.equity?.length, equityLength: result.equity?.length || result.performanceData?.length,
tradesLength: result.trades?.length tradesLength: result.trades?.length
}); });
} }
} }
const equityData = (result.equity || []) const equityData = result.equity
.filter(e => e && e.date && e.value != null) ? result.equity
.map(e => ({ .filter(e => e && e.date && e.value != null)
time: new Date(e.date).getTime() / 1000, .map(e => ({
value: e.value time: new Date(e.date).getTime() / 1000,
})); value: e.value
}))
: result.performanceData
?.filter(p => p && p.timestamp && p.portfolioValue != null)
.map(p => ({
time: new Date(p.timestamp).getTime() / 1000,
value: p.portfolioValue
})) || [];
// Find trades for this symbol // Find trades for this symbol
const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || []; const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || [];
@ -51,15 +86,17 @@ export const BacktestChart = memo(function BacktestChart({ result, isLoading }:
const tradeMarkers = symbolTrades const tradeMarkers = symbolTrades
.filter(trade => { .filter(trade => {
// Check multiple possible field names // Check multiple possible field names
const hasPrice = trade.price != null || trade.entryPrice != null; const extTrade = trade as ExtendedTrade;
const hasTime = trade.timestamp != null || trade.entryDate != null || trade.date != null; const hasPrice = extTrade.price != null || extTrade.entryPrice != null;
const hasTime = extTrade.timestamp != null || extTrade.entryDate != null || extTrade.date != null;
return hasPrice && hasTime; return hasPrice && hasTime;
}) })
.map(trade => { .map(trade => {
// Use whatever field names are present // Use whatever field names are present
const price = trade.price || trade.entryPrice; const extTrade = trade as ExtendedTrade;
const timestamp = trade.timestamp || trade.entryDate || trade.date; const price = extTrade.price || extTrade.entryPrice || 0;
const side = trade.side || (trade.quantity > 0 ? 'buy' : 'sell'); const timestamp = extTrade.timestamp || extTrade.entryDate || extTrade.date || '';
const side = extTrade.side || (extTrade.quantity > 0 ? 'buy' : 'sell');
return { return {
time: new Date(timestamp).getTime() / 1000, time: new Date(timestamp).getTime() / 1000,
@ -140,8 +177,8 @@ export const BacktestChart = memo(function BacktestChart({ result, isLoading }:
]} ]}
tradeMarkers={chartData.tradeMarkers} tradeMarkers={chartData.tradeMarkers}
height={height} height={height}
chartId={`backtest-${result?.runId || 'default'}`} chartId={`backtest-${result?.runId || result?.id || 'default'}`}
key={result?.runId || 'default'} key={result?.runId || result?.id || 'default'}
/> />
)} )}
</ChartContainer> </ChartContainer>

View file

@ -37,22 +37,22 @@ export function BacktestMetrics({ result, isLoading }: BacktestMetricsProps) {
<MetricsCard <MetricsCard
title="Total Return" title="Total Return"
value={`${((metrics.totalReturn ?? 0) * 100).toFixed(2)}%`} value={`${((metrics.totalReturn ?? 0) * 100).toFixed(2)}%`}
color={(metrics.totalReturn ?? 0) >= 0 ? 'success' : 'error'} trend={(metrics.totalReturn ?? 0) >= 0 ? 'up' : 'down'}
/> />
<MetricsCard <MetricsCard
title="Sharpe Ratio" title="Sharpe Ratio"
value={(metrics.sharpeRatio ?? 0).toFixed(2)} value={(metrics.sharpeRatio ?? 0).toFixed(2)}
color={(metrics.sharpeRatio ?? 0) > 1 ? 'success' : (metrics.sharpeRatio ?? 0) > 0 ? 'warning' : 'error'} trend={(metrics.sharpeRatio ?? 0) > 0 ? 'up' : 'down'}
/> />
<MetricsCard <MetricsCard
title="Max Drawdown" title="Max Drawdown"
value={`${((metrics.maxDrawdown ?? 0) * 100).toFixed(2)}%`} value={`${((metrics.maxDrawdown ?? 0) * 100).toFixed(2)}%`}
color={(metrics.maxDrawdown ?? 0) > -0.2 ? 'warning' : 'error'} trend='down'
/> />
<MetricsCard <MetricsCard
title="Win Rate" title="Win Rate"
value={`${((metrics.winRate ?? 0) * 100).toFixed(1)}%`} value={`${((metrics.winRate ?? 0) * 100).toFixed(1)}%`}
color={(metrics.winRate ?? 0) > 0.5 ? 'success' : 'warning'} trend={(metrics.winRate ?? 0) > 0.5 ? 'up' : 'down'}
/> />
<MetricsCard <MetricsCard
title="Total Trades" title="Total Trades"
@ -61,7 +61,7 @@ export function BacktestMetrics({ result, isLoading }: BacktestMetricsProps) {
<MetricsCard <MetricsCard
title="Profit Factor" title="Profit Factor"
value={(metrics.profitFactor ?? 0).toFixed(2)} value={(metrics.profitFactor ?? 0).toFixed(2)}
color={(metrics.profitFactor ?? 0) > 1.5 ? 'success' : (metrics.profitFactor ?? 0) > 1 ? 'warning' : 'error'} trend={(metrics.profitFactor ?? 0) > 1 ? 'up' : 'down'}
/> />
</div> </div>

View file

@ -7,12 +7,16 @@ export function calculateSMA(data: CandlestickData[], period: number): LineData[
for (let i = period - 1; i < data.length; i++) { for (let i = period - 1; i < data.length; i++) {
let sum = 0; let sum = 0;
for (let j = 0; j < period; j++) { for (let j = 0; j < period; j++) {
sum += data[i - j].close; const item = data[i - j];
if (item) sum += item.close;
}
const current = data[i];
if (current) {
result.push({
time: current.time,
value: parseFloat((sum / period).toFixed(2)),
});
} }
result.push({
time: data[i].time,
value: parseFloat((sum / period).toFixed(2)),
});
} }
return result; return result;
@ -26,22 +30,29 @@ export function calculateEMA(data: CandlestickData[], period: number): LineData[
// Start with SMA for the first period // Start with SMA for the first period
let sum = 0; let sum = 0;
for (let i = 0; i < period; i++) { for (let i = 0; i < period; i++) {
sum += data[i].close; const item = data[i];
if (item) sum += item.close;
} }
let ema = sum / period; let ema = sum / period;
result.push({ const firstItem = data[period - 1];
time: data[period - 1].time, if (firstItem) {
value: parseFloat(ema.toFixed(2)), result.push({
}); time: firstItem.time,
value: parseFloat(ema.toFixed(2)),
});
}
// Calculate EMA for the rest // Calculate EMA for the rest
for (let i = period; i < data.length; i++) { for (let i = period; i < data.length; i++) {
ema = (data[i].close - ema) * multiplier + ema; const current = data[i];
result.push({ if (current) {
time: data[i].time, ema = (current.close - ema) * multiplier + ema;
value: parseFloat(ema.toFixed(2)), result.push({
}); time: current.time,
value: parseFloat(ema.toFixed(2)),
});
}
} }
return result; return result;
@ -55,26 +66,34 @@ export function calculateBollingerBands(data: CandlestickData[], period: number
for (let i = period - 1; i < data.length; i++) { for (let i = period - 1; i < data.length; i++) {
let sumSquaredDiff = 0; let sumSquaredDiff = 0;
const smaValue = sma[i - (period - 1)].value; const smaItem = sma[i - (period - 1)];
if (!smaItem) continue;
const smaValue = smaItem.value;
for (let j = 0; j < period; j++) { for (let j = 0; j < period; j++) {
const diff = data[i - j].close - smaValue; const item = data[i - j];
sumSquaredDiff += diff * diff; if (item) {
const diff = item.close - smaValue;
sumSquaredDiff += diff * diff;
}
} }
const standardDeviation = Math.sqrt(sumSquaredDiff / period); const standardDeviation = Math.sqrt(sumSquaredDiff / period);
const upperBand = smaValue + (standardDeviation * stdDev); const upperBand = smaValue + (standardDeviation * stdDev);
const lowerBand = smaValue - (standardDeviation * stdDev); const lowerBand = smaValue - (standardDeviation * stdDev);
upper.push({ const current = data[i];
time: data[i].time, if (current) {
value: parseFloat(upperBand.toFixed(2)), upper.push({
}); time: current.time,
value: parseFloat(upperBand.toFixed(2)),
lower.push({ });
time: data[i].time,
value: parseFloat(lowerBand.toFixed(2)), lower.push({
}); time: current.time,
value: parseFloat(lowerBand.toFixed(2)),
});
}
} }
return { upper, middle: sma, lower }; return { upper, middle: sma, lower };
@ -91,11 +110,15 @@ export function calculateRSI(data: CandlestickData[], period: number = 14): Line
let avgLoss = 0; let avgLoss = 0;
for (let i = 1; i <= period; i++) { for (let i = 1; i <= period; i++) {
const change = data[i].close - data[i - 1].close; const current = data[i];
if (change > 0) { const prev = data[i - 1];
avgGain += change; if (current && prev) {
} else { const change = current.close - prev.close;
avgLoss += Math.abs(change); if (change > 0) {
avgGain += change;
} else {
avgLoss += Math.abs(change);
}
} }
} }
@ -104,7 +127,10 @@ export function calculateRSI(data: CandlestickData[], period: number = 14): Line
// Calculate RSI for each period // Calculate RSI for each period
for (let i = period; i < data.length; i++) { for (let i = period; i < data.length; i++) {
const change = data[i].close - data[i - 1].close; const current = data[i];
const prev = data[i - 1];
if (!current || !prev) continue;
const change = current.close - prev.close;
const gain = change > 0 ? change : 0; const gain = change > 0 ? change : 0;
const loss = change < 0 ? Math.abs(change) : 0; const loss = change < 0 ? Math.abs(change) : 0;
@ -115,7 +141,7 @@ export function calculateRSI(data: CandlestickData[], period: number = 14): Line
const rsi = 100 - (100 / (1 + rs)); const rsi = 100 - (100 / (1 + rs));
result.push({ result.push({
time: data[i].time, time: current.time,
value: parseFloat(rsi.toFixed(2)), value: parseFloat(rsi.toFixed(2)),
}); });
} }
@ -155,17 +181,23 @@ export function calculateMACD(
const multiplier = 2 / (signalPeriod + 1); const multiplier = 2 / (signalPeriod + 1);
let ema = macdData.slice(0, signalPeriod).reduce((a, b) => a + b, 0) / signalPeriod; let ema = macdData.slice(0, signalPeriod).reduce((a, b) => a + b, 0) / signalPeriod;
signalLine.push({ const firstMacd = macdLine[signalPeriod - 1];
time: macdLine[signalPeriod - 1].time, if (firstMacd) {
value: parseFloat(ema.toFixed(2)), signalLine.push({
}); time: firstMacd.time,
value: parseFloat(ema.toFixed(2)),
});
}
for (let i = signalPeriod; i < macdData.length; i++) { for (let i = signalPeriod; i < macdData.length; i++) {
ema = (macdData[i] - ema) * multiplier + ema; ema = (macdData[i] - ema) * multiplier + ema;
signalLine.push({ const macdItem = macdLine[i];
time: macdLine[i].time, if (macdItem) {
value: parseFloat(ema.toFixed(2)), signalLine.push({
}); time: macdItem.time,
value: parseFloat(ema.toFixed(2)),
});
}
} }
} }
@ -191,15 +223,17 @@ export function calculateVWAP(data: CandlestickData[]): LineData[] {
let cumulativeVolume = 0; let cumulativeVolume = 0;
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const typicalPrice = (data[i].high + data[i].low + data[i].close) / 3; const current = data[i];
const volume = data[i].volume || 0; if (!current) continue;
const typicalPrice = (current.high + current.low + current.close) / 3;
const volume = current.volume || 0;
cumulativeTPV += typicalPrice * volume; cumulativeTPV += typicalPrice * volume;
cumulativeVolume += volume; cumulativeVolume += volume;
if (cumulativeVolume > 0) { if (cumulativeVolume > 0) {
result.push({ result.push({
time: data[i].time, time: current.time,
value: parseFloat((cumulativeTPV / cumulativeVolume).toFixed(2)), value: parseFloat((cumulativeTPV / cumulativeVolume).toFixed(2)),
}); });
} }
@ -211,21 +245,24 @@ export function calculateVWAP(data: CandlestickData[]): LineData[] {
// Stochastic Oscillator // Stochastic Oscillator
export function calculateStochastic(data: CandlestickData[], period: number = 14, smoothK: number = 3, smoothD: number = 3) { export function calculateStochastic(data: CandlestickData[], period: number = 14, smoothK: number = 3, smoothD: number = 3) {
const kValues: LineData[] = []; const kValues: LineData[] = [];
const dValues: LineData[] = [];
// Calculate %K // Calculate %K
for (let i = period - 1; i < data.length; i++) { for (let i = period - 1; i < data.length; i++) {
let lowestLow = data[i].low; const current = data[i];
let highestHigh = data[i].high; if (!current) continue;
let lowestLow = current.low;
let highestHigh = current.high;
for (let j = 1; j < period; j++) { for (let j = 1; j < period; j++) {
lowestLow = Math.min(lowestLow, data[i - j].low); const prev = data[i - j];
highestHigh = Math.max(highestHigh, data[i - j].high); if (!prev) continue;
lowestLow = Math.min(lowestLow, prev.low);
highestHigh = Math.max(highestHigh, prev.high);
} }
const k = ((data[i].close - lowestLow) / (highestHigh - lowestLow)) * 100; const k = ((current.close - lowestLow) / (highestHigh - lowestLow)) * 100;
kValues.push({ kValues.push({
time: data[i].time, time: current.time,
value: parseFloat(k.toFixed(2)), value: parseFloat(k.toFixed(2)),
}); });
} }

View file

@ -162,22 +162,22 @@ export function PortfolioTable() {
sharpe: Math.random() * 3, sharpe: Math.random() * 3,
alpha: (Math.random() - 0.5) * 20, alpha: (Math.random() - 0.5) * 20,
correlation: (Math.random() - 0.5) * 2, correlation: (Math.random() - 0.5) * 2,
sector: sectors[Math.floor(Math.random() * sectors.length)], sector: sectors[Math.floor(Math.random() * sectors.length)] || 'Technology',
industry: industries[Math.floor(Math.random() * industries.length)], industry: industries[Math.floor(Math.random() * industries.length)] || 'Software',
country: countries[Math.floor(Math.random() * countries.length)], country: countries[Math.floor(Math.random() * countries.length)] || 'USA',
exchange: exchanges[Math.floor(Math.random() * exchanges.length)], exchange: exchanges[Math.floor(Math.random() * exchanges.length)] || 'NYSE',
currency: currencies[Math.floor(Math.random() * currencies.length)], currency: currencies[Math.floor(Math.random() * currencies.length)] || 'USD',
lastUpdate: new Date(Date.now() - Math.random() * 86400000).toISOString(), lastUpdate: new Date(Date.now() - Math.random() * 86400000).toISOString(),
analyst1: analysts[Math.floor(Math.random() * analysts.length)], analyst1: analysts[Math.floor(Math.random() * analysts.length)] || 'Goldman Sachs',
analyst2: analysts[Math.floor(Math.random() * analysts.length)], analyst2: analysts[Math.floor(Math.random() * analysts.length)] || 'Morgan Stanley',
analyst3: analysts[Math.floor(Math.random() * analysts.length)], analyst3: analysts[Math.floor(Math.random() * analysts.length)] || 'JPMorgan',
rating1: Math.random() * 5 + 1, rating1: Math.random() * 5 + 1,
rating2: Math.random() * 5 + 1, rating2: Math.random() * 5 + 1,
rating3: Math.random() * 5 + 1, rating3: Math.random() * 5 + 1,
target1: basePrice + (Math.random() - 0.3) * 50, target1: basePrice + (Math.random() - 0.3) * 50,
target2: basePrice + (Math.random() - 0.3) * 50, target2: basePrice + (Math.random() - 0.3) * 50,
target3: basePrice + (Math.random() - 0.3) * 50, target3: basePrice + (Math.random() - 0.3) * 50,
risk: risks[Math.floor(Math.random() * risks.length)], risk: risks[Math.floor(Math.random() * risks.length)] || 'Medium',
esg: Math.random() * 100, esg: Math.random() * 100,
}; };
}); });

View file

@ -27,6 +27,7 @@ export function ExchangesPage() {
}, 5000); }, 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
return undefined;
}, [syncStatus.type]); }, [syncStatus.type]);
const handleSync = async () => { const handleSync = async () => {

View file

@ -5,6 +5,7 @@ export { OperationContext } from './operation-context';
export { calculatePoolSize, getServicePoolSize, getHandlerPoolSize } from './pool-size-calculator'; export { calculatePoolSize, getServicePoolSize, getHandlerPoolSize } from './pool-size-calculator';
export { ServiceLifecycleManager } from './utils/lifecycle'; export { ServiceLifecycleManager } from './utils/lifecycle';
export { HandlerScanner } from './scanner/handler-scanner'; export { HandlerScanner } from './scanner/handler-scanner';
export type { IServiceContainer } from '@stock-bot/types';
// Export schemas // Export schemas
export { export {