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
|
||||
// Use utility function to handle US symbols and EUFUND special case
|
||||
const exchangeSuffix = getEodExchangeSuffix(exchange, country);
|
||||
|
||||
// eodSearchCode already contains the symbol with exchange suffix (e.g., AAPL.US)
|
||||
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('fmt', 'json');
|
||||
|
||||
|
|
|
|||
|
|
@ -139,18 +139,17 @@ export async function fetchBulkFundamentals(
|
|||
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 symbol = symbolDoc.Code;
|
||||
const exchange = symbolDoc.eodExchange || symbolDoc.Exchange;
|
||||
const country = symbolDoc.Country;
|
||||
const eodSearchCode = symbolDoc.eodSearchCode;
|
||||
|
||||
if (!acc[exchange]) {
|
||||
acc[exchange] = [];
|
||||
}
|
||||
// Use utility function to handle US symbols and EUFUND special case
|
||||
const exchangeSuffix = getEodExchangeSuffix(exchange, country);
|
||||
acc[exchange].push(`${symbol}.${exchangeSuffix}`);
|
||||
// eodSearchCode already has the correct format (e.g., AAPL.US)
|
||||
acc[exchange].push(eodSearchCode);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
|
||||
|
|
@ -309,10 +308,8 @@ export async function fetchSingleFundamentals(
|
|||
}
|
||||
|
||||
// Build URL for single fundamentals endpoint
|
||||
// Use utility function to handle US symbols and EUFUND special case
|
||||
const exchangeSuffix = getEodExchangeSuffix(exchange, country);
|
||||
|
||||
const url = new URL(`https://eodhd.com/api/fundamentals/${symbol}.${exchangeSuffix}`);
|
||||
// eodSearchCode already contains the symbol with exchange suffix (e.g., AAPL.US)
|
||||
const url = new URL(`https://eodhd.com/api/fundamentals/${eodSearchCode}`);
|
||||
url.searchParams.append('api_token', apiKey);
|
||||
url.searchParams.append('fmt', 'json');
|
||||
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ export async function crawlIntraday(
|
|||
finished: updateData.finished
|
||||
};
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import { createEODOperationRegistry } from './shared';
|
|||
],
|
||||
})
|
||||
export class EodHandler extends BaseHandler<DataIngestionServices> {
|
||||
public operationRegistry: OperationRegistry;
|
||||
public operationRegistry!: OperationRegistry;
|
||||
|
||||
constructor(services: any) {
|
||||
super(services);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { createQMOperationRegistry } from './shared/operation-provider';
|
|||
@Handler('qm')
|
||||
@Disabled() // Disable by default, enable specific operations as needed
|
||||
export class QMHandler extends BaseHandler<DataIngestionServices> {
|
||||
public operationRegistry: OperationRegistry;
|
||||
public operationRegistry!: OperationRegistry;
|
||||
|
||||
constructor(services: any) {
|
||||
super(services); // Handler name read from @Handler decorator
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import { BaseOperationProvider } from './BaseOperationProvider';
|
|||
import { OperationTracker } from './OperationTracker';
|
||||
import type {
|
||||
OperationComponentOptions,
|
||||
OperationUpdate,
|
||||
StaleSymbolOptions,
|
||||
BulkOperationUpdate,
|
||||
OperationConfig,
|
||||
OperationStats,
|
||||
OperationConfig
|
||||
OperationUpdate,
|
||||
StaleSymbolOptions
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -161,7 +161,20 @@ export class PerformanceAnalyzer {
|
|||
sortinoRatio,
|
||||
calmarRatio,
|
||||
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,
|
||||
kurtosis,
|
||||
tailRatio,
|
||||
|
|
@ -178,9 +191,8 @@ export class PerformanceAnalyzer {
|
|||
if (this.equityCurve.length === 0) {
|
||||
return {
|
||||
maxDrawdown: 0,
|
||||
averageDrawdown: 0,
|
||||
maxDrawdownDuration: 0,
|
||||
underwaterTime: 0,
|
||||
underwaterCurve: [],
|
||||
drawdownPeriods: [],
|
||||
currentDrawdown: 0
|
||||
};
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export class BacktestEngine extends EventEmitter {
|
|||
private lastProcessTime = 0;
|
||||
private dataManager: DataManager;
|
||||
private marketSimulator: MarketSimulator;
|
||||
private performanceAnalyzer: PerformanceAnalyzer;
|
||||
private performanceAnalyzer!: PerformanceAnalyzer;
|
||||
private microstructures: Map<string, MarketMicrostructure> = new Map();
|
||||
private container: IServiceContainer;
|
||||
private runId: string | null = null;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component, ReactNode } from 'react';
|
||||
import React, { Component, type ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
|
@ -20,11 +20,11 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||
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);
|
||||
}
|
||||
|
||||
render() {
|
||||
override render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
this.props.fallback || (
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ export function Chart({
|
|||
return `${chartId}-${Date.now()}`;
|
||||
}, [chartId])
|
||||
const mountedRef = useRef(true);
|
||||
const lastChartIdRef = useRef(chartId);
|
||||
|
||||
// Track component mount state
|
||||
useEffect(() => {
|
||||
|
|
@ -126,13 +125,13 @@ export function Chart({
|
|||
|
||||
// Use requestAnimationFrame to ensure DOM is ready and avoid conflicts
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
const initTimeout = setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
if (!chartContainerRef.current || isCleanedUp || !mountedRef.current) return;
|
||||
|
||||
// Create chart using the chart manager
|
||||
const chart = createChart(uniqueChartId, chartContainerRef.current, {
|
||||
width: chartContainerRef.current.clientWidth,
|
||||
height: height,
|
||||
height: typeof height === 'string' ? parseInt(height) : height,
|
||||
layout: {
|
||||
background: {
|
||||
type: LightweightCharts.ColorType.Solid,
|
||||
|
|
@ -188,7 +187,7 @@ export function Chart({
|
|||
time: timeInSeconds as LightweightCharts.Time
|
||||
};
|
||||
})
|
||||
.filter((item, index) => {
|
||||
.filter((item) => {
|
||||
if (seen.has(item.time)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -201,7 +200,7 @@ export function Chart({
|
|||
// Create main series
|
||||
let mainSeries: LightweightCharts.ISeriesApi<any> | null = null;
|
||||
|
||||
if (type === 'candlestick' && data[0].open !== undefined) {
|
||||
if (type === 'candlestick' && data[0]?.open !== undefined) {
|
||||
mainSeries = chart.addCandlestickSeries({
|
||||
upColor: '#10b981',
|
||||
downColor: '#ef4444',
|
||||
|
|
@ -212,7 +211,7 @@ export function Chart({
|
|||
});
|
||||
const validData = validateAndFilterData(data);
|
||||
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({
|
||||
color: '#3b82f6',
|
||||
lineWidth: 2,
|
||||
|
|
@ -278,7 +277,7 @@ export function Chart({
|
|||
overlayData.forEach((overlay, index) => {
|
||||
const series = chart.addLineSeries({
|
||||
color: overlay.color || ['#ff9800', '#4caf50', '#9c27b0', '#f44336'][index % 4],
|
||||
lineWidth: overlay.lineWidth || 2,
|
||||
lineWidth: (overlay.lineWidth || 2) as LightweightCharts.LineWidth,
|
||||
title: overlay.name,
|
||||
priceScaleId: index === 0 ? '' : `overlay-${index}`, // First overlay uses main scale
|
||||
});
|
||||
|
|
@ -371,7 +370,7 @@ export function Chart({
|
|||
});
|
||||
} catch (error) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface TableColumn<T = Record<string, unknown>> {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -38,13 +38,12 @@ export function BacktestDetailPageV2() {
|
|||
createRun,
|
||||
pauseRun,
|
||||
resumeRun,
|
||||
cancelRun,
|
||||
updateRunSpeed,
|
||||
selectRun,
|
||||
} = useBacktestV2();
|
||||
|
||||
// WebSocket connection for real-time updates
|
||||
const { isConnected } = useWebSocket({
|
||||
useWebSocket({
|
||||
runId: currentRun?.id || null,
|
||||
onProgress: (progress, currentDate) => {
|
||||
// Update the run progress in the UI
|
||||
|
|
@ -60,7 +59,7 @@ export function BacktestDetailPageV2() {
|
|||
loadBacktest(id);
|
||||
}
|
||||
},
|
||||
onCompleted: async (results) => {
|
||||
onCompleted: async () => {
|
||||
// When run completes, reload to get the final results
|
||||
if (currentRun?.id) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,36 @@ import { useState, useMemo, memo } from 'react';
|
|||
import { Chart } from '../../../components/charts/Chart';
|
||||
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 {
|
||||
result: BacktestResult | null;
|
||||
result: ExtendedBacktestResult | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -13,36 +41,43 @@ export const BacktestChart = memo(function BacktestChart({ result, isLoading }:
|
|||
const [selectedSymbol, setSelectedSymbol] = useState<string>('');
|
||||
|
||||
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 ohlcData = result.ohlcData[symbol] || [];
|
||||
const ohlcData = result.ohlcData?.[symbol] || [];
|
||||
|
||||
// Remove excessive logging in production
|
||||
// Log only on significant changes
|
||||
if (process.env.NODE_ENV === 'development' && ohlcData.length > 0) {
|
||||
// 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) {
|
||||
(window as any).__lastDataHash = dataHash;
|
||||
console.log('BacktestChart data updated:', {
|
||||
symbols,
|
||||
selectedSymbol,
|
||||
symbol,
|
||||
ohlcDataKeys: Object.keys(result.ohlcData),
|
||||
equityLength: result.equity?.length,
|
||||
ohlcDataKeys: result.ohlcData ? Object.keys(result.ohlcData) : [],
|
||||
equityLength: result.equity?.length || result.performanceData?.length,
|
||||
tradesLength: result.trades?.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const equityData = (result.equity || [])
|
||||
const equityData = result.equity
|
||||
? result.equity
|
||||
.filter(e => e && e.date && e.value != null)
|
||||
.map(e => ({
|
||||
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
|
||||
const symbolTrades = result.trades?.filter(t => t.symbol === symbol) || [];
|
||||
|
|
@ -51,15 +86,17 @@ export const BacktestChart = memo(function BacktestChart({ result, isLoading }:
|
|||
const tradeMarkers = symbolTrades
|
||||
.filter(trade => {
|
||||
// Check multiple possible field names
|
||||
const hasPrice = trade.price != null || trade.entryPrice != null;
|
||||
const hasTime = trade.timestamp != null || trade.entryDate != null || trade.date != null;
|
||||
const extTrade = trade as ExtendedTrade;
|
||||
const hasPrice = extTrade.price != null || extTrade.entryPrice != null;
|
||||
const hasTime = extTrade.timestamp != null || extTrade.entryDate != null || extTrade.date != null;
|
||||
return hasPrice && hasTime;
|
||||
})
|
||||
.map(trade => {
|
||||
// Use whatever field names are present
|
||||
const price = trade.price || trade.entryPrice;
|
||||
const timestamp = trade.timestamp || trade.entryDate || trade.date;
|
||||
const side = trade.side || (trade.quantity > 0 ? 'buy' : 'sell');
|
||||
const extTrade = trade as ExtendedTrade;
|
||||
const price = extTrade.price || extTrade.entryPrice || 0;
|
||||
const timestamp = extTrade.timestamp || extTrade.entryDate || extTrade.date || '';
|
||||
const side = extTrade.side || (extTrade.quantity > 0 ? 'buy' : 'sell');
|
||||
|
||||
return {
|
||||
time: new Date(timestamp).getTime() / 1000,
|
||||
|
|
@ -140,8 +177,8 @@ export const BacktestChart = memo(function BacktestChart({ result, isLoading }:
|
|||
]}
|
||||
tradeMarkers={chartData.tradeMarkers}
|
||||
height={height}
|
||||
chartId={`backtest-${result?.runId || 'default'}`}
|
||||
key={result?.runId || 'default'}
|
||||
chartId={`backtest-${result?.runId || result?.id || 'default'}`}
|
||||
key={result?.runId || result?.id || 'default'}
|
||||
/>
|
||||
)}
|
||||
</ChartContainer>
|
||||
|
|
|
|||
|
|
@ -37,22 +37,22 @@ export function BacktestMetrics({ result, isLoading }: BacktestMetricsProps) {
|
|||
<MetricsCard
|
||||
title="Total Return"
|
||||
value={`${((metrics.totalReturn ?? 0) * 100).toFixed(2)}%`}
|
||||
color={(metrics.totalReturn ?? 0) >= 0 ? 'success' : 'error'}
|
||||
trend={(metrics.totalReturn ?? 0) >= 0 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Sharpe Ratio"
|
||||
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
|
||||
title="Max Drawdown"
|
||||
value={`${((metrics.maxDrawdown ?? 0) * 100).toFixed(2)}%`}
|
||||
color={(metrics.maxDrawdown ?? 0) > -0.2 ? 'warning' : 'error'}
|
||||
trend='down'
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Win Rate"
|
||||
value={`${((metrics.winRate ?? 0) * 100).toFixed(1)}%`}
|
||||
color={(metrics.winRate ?? 0) > 0.5 ? 'success' : 'warning'}
|
||||
trend={(metrics.winRate ?? 0) > 0.5 ? 'up' : 'down'}
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Total Trades"
|
||||
|
|
@ -61,7 +61,7 @@ export function BacktestMetrics({ result, isLoading }: BacktestMetricsProps) {
|
|||
<MetricsCard
|
||||
title="Profit Factor"
|
||||
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>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,17 @@ export function calculateSMA(data: CandlestickData[], period: number): 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;
|
||||
const item = data[i - j];
|
||||
if (item) sum += item.close;
|
||||
}
|
||||
const current = data[i];
|
||||
if (current) {
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
time: current.time,
|
||||
value: parseFloat((sum / period).toFixed(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -26,23 +30,30 @@ export function calculateEMA(data: CandlestickData[], period: number): LineData[
|
|||
// Start with SMA for the first period
|
||||
let sum = 0;
|
||||
for (let i = 0; i < period; i++) {
|
||||
sum += data[i].close;
|
||||
const item = data[i];
|
||||
if (item) sum += item.close;
|
||||
}
|
||||
let ema = sum / period;
|
||||
|
||||
const firstItem = data[period - 1];
|
||||
if (firstItem) {
|
||||
result.push({
|
||||
time: data[period - 1].time,
|
||||
time: firstItem.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;
|
||||
const current = data[i];
|
||||
if (current) {
|
||||
ema = (current.close - ema) * multiplier + ema;
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
time: current.time,
|
||||
value: parseFloat(ema.toFixed(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -55,27 +66,35 @@ export function calculateBollingerBands(data: CandlestickData[], period: number
|
|||
|
||||
for (let i = period - 1; i < data.length; i++) {
|
||||
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++) {
|
||||
const diff = data[i - j].close - smaValue;
|
||||
const item = data[i - j];
|
||||
if (item) {
|
||||
const diff = item.close - smaValue;
|
||||
sumSquaredDiff += diff * diff;
|
||||
}
|
||||
}
|
||||
|
||||
const standardDeviation = Math.sqrt(sumSquaredDiff / period);
|
||||
const upperBand = smaValue + (standardDeviation * stdDev);
|
||||
const lowerBand = smaValue - (standardDeviation * stdDev);
|
||||
|
||||
const current = data[i];
|
||||
if (current) {
|
||||
upper.push({
|
||||
time: data[i].time,
|
||||
time: current.time,
|
||||
value: parseFloat(upperBand.toFixed(2)),
|
||||
});
|
||||
|
||||
lower.push({
|
||||
time: data[i].time,
|
||||
time: current.time,
|
||||
value: parseFloat(lowerBand.toFixed(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { upper, middle: sma, lower };
|
||||
}
|
||||
|
|
@ -91,20 +110,27 @@ export function calculateRSI(data: CandlestickData[], period: number = 14): Line
|
|||
let avgLoss = 0;
|
||||
|
||||
for (let i = 1; i <= period; i++) {
|
||||
const change = data[i].close - data[i - 1].close;
|
||||
const current = data[i];
|
||||
const prev = data[i - 1];
|
||||
if (current && prev) {
|
||||
const change = current.close - prev.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 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 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));
|
||||
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
time: current.time,
|
||||
value: parseFloat(rsi.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
|
@ -155,19 +181,25 @@ export function calculateMACD(
|
|||
const multiplier = 2 / (signalPeriod + 1);
|
||||
let ema = macdData.slice(0, signalPeriod).reduce((a, b) => a + b, 0) / signalPeriod;
|
||||
|
||||
const firstMacd = macdLine[signalPeriod - 1];
|
||||
if (firstMacd) {
|
||||
signalLine.push({
|
||||
time: macdLine[signalPeriod - 1].time,
|
||||
time: firstMacd.time,
|
||||
value: parseFloat(ema.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = signalPeriod; i < macdData.length; i++) {
|
||||
ema = (macdData[i] - ema) * multiplier + ema;
|
||||
const macdItem = macdLine[i];
|
||||
if (macdItem) {
|
||||
signalLine.push({
|
||||
time: macdLine[i].time,
|
||||
time: macdItem.time,
|
||||
value: parseFloat(ema.toFixed(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate histogram
|
||||
const histogram: LineData[] = [];
|
||||
|
|
@ -191,15 +223,17 @@ export function calculateVWAP(data: CandlestickData[]): LineData[] {
|
|||
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;
|
||||
const current = data[i];
|
||||
if (!current) continue;
|
||||
const typicalPrice = (current.high + current.low + current.close) / 3;
|
||||
const volume = current.volume || 0;
|
||||
|
||||
cumulativeTPV += typicalPrice * volume;
|
||||
cumulativeVolume += volume;
|
||||
|
||||
if (cumulativeVolume > 0) {
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
time: current.time,
|
||||
value: parseFloat((cumulativeTPV / cumulativeVolume).toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
|
@ -211,21 +245,24 @@ export function calculateVWAP(data: CandlestickData[]): LineData[] {
|
|||
// 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;
|
||||
const current = data[i];
|
||||
if (!current) continue;
|
||||
let lowestLow = current.low;
|
||||
let highestHigh = current.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 prev = data[i - j];
|
||||
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({
|
||||
time: data[i].time,
|
||||
time: current.time,
|
||||
value: parseFloat(k.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,22 +162,22 @@ export function PortfolioTable() {
|
|||
sharpe: Math.random() * 3,
|
||||
alpha: (Math.random() - 0.5) * 20,
|
||||
correlation: (Math.random() - 0.5) * 2,
|
||||
sector: sectors[Math.floor(Math.random() * sectors.length)],
|
||||
industry: industries[Math.floor(Math.random() * industries.length)],
|
||||
country: countries[Math.floor(Math.random() * countries.length)],
|
||||
exchange: exchanges[Math.floor(Math.random() * exchanges.length)],
|
||||
currency: currencies[Math.floor(Math.random() * currencies.length)],
|
||||
sector: sectors[Math.floor(Math.random() * sectors.length)] || 'Technology',
|
||||
industry: industries[Math.floor(Math.random() * industries.length)] || 'Software',
|
||||
country: countries[Math.floor(Math.random() * countries.length)] || 'USA',
|
||||
exchange: exchanges[Math.floor(Math.random() * exchanges.length)] || 'NYSE',
|
||||
currency: currencies[Math.floor(Math.random() * currencies.length)] || 'USD',
|
||||
lastUpdate: new Date(Date.now() - Math.random() * 86400000).toISOString(),
|
||||
analyst1: analysts[Math.floor(Math.random() * analysts.length)],
|
||||
analyst2: analysts[Math.floor(Math.random() * analysts.length)],
|
||||
analyst3: 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)] || 'Morgan Stanley',
|
||||
analyst3: analysts[Math.floor(Math.random() * analysts.length)] || 'JPMorgan',
|
||||
rating1: Math.random() * 5 + 1,
|
||||
rating2: Math.random() * 5 + 1,
|
||||
rating3: Math.random() * 5 + 1,
|
||||
target1: basePrice + (Math.random() - 0.3) * 50,
|
||||
target2: 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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export function ExchangesPage() {
|
|||
}, 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, [syncStatus.type]);
|
||||
|
||||
const handleSync = async () => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export { OperationContext } from './operation-context';
|
|||
export { calculatePoolSize, getServicePoolSize, getHandlerPoolSize } from './pool-size-calculator';
|
||||
export { ServiceLifecycleManager } from './utils/lifecycle';
|
||||
export { HandlerScanner } from './scanner/handler-scanner';
|
||||
export type { IServiceContainer } from '@stock-bot/types';
|
||||
|
||||
// Export schemas
|
||||
export {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue