work on clean up and switched all to use eodSearchCode
This commit is contained in:
parent
d68268b722
commit
e341cc0226
19 changed files with 206 additions and 127 deletions
|
|
@ -1 +0,0 @@
|
||||||
../../../node_modules
|
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 || (
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue