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
// 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');

View file

@ -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');

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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

View file

@ -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';
/**

View file

@ -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
};

View file

@ -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;

View file

@ -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 || (

View file

@ -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);
}
}

View file

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

View file

@ -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 {

View file

@ -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 || [])
.filter(e => e && e.date && e.value != null)
.map(e => ({
time: new Date(e.date).getTime() / 1000,
value: e.value
}));
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>

View file

@ -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>

View file

@ -7,12 +7,16 @@ 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: current.time,
value: parseFloat((sum / period).toFixed(2)),
});
}
result.push({
time: data[i].time,
value: parseFloat((sum / period).toFixed(2)),
});
}
return result;
@ -26,22 +30,29 @@ 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;
result.push({
time: data[period - 1].time,
value: parseFloat(ema.toFixed(2)),
});
const firstItem = data[period - 1];
if (firstItem) {
result.push({
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;
result.push({
time: data[i].time,
value: parseFloat(ema.toFixed(2)),
});
const current = data[i];
if (current) {
ema = (current.close - ema) * multiplier + ema;
result.push({
time: current.time,
value: parseFloat(ema.toFixed(2)),
});
}
}
return result;
@ -55,26 +66,34 @@ 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;
sumSquaredDiff += diff * diff;
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);
upper.push({
time: data[i].time,
value: parseFloat(upperBand.toFixed(2)),
});
lower.push({
time: data[i].time,
value: parseFloat(lowerBand.toFixed(2)),
});
const current = data[i];
if (current) {
upper.push({
time: current.time,
value: parseFloat(upperBand.toFixed(2)),
});
lower.push({
time: current.time,
value: parseFloat(lowerBand.toFixed(2)),
});
}
}
return { upper, middle: sma, lower };
@ -91,11 +110,15 @@ 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;
if (change > 0) {
avgGain += change;
} else {
avgLoss += Math.abs(change);
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);
}
}
}
@ -104,7 +127,10 @@ export function calculateRSI(data: CandlestickData[], period: number = 14): Line
// 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,17 +181,23 @@ export function calculateMACD(
const multiplier = 2 / (signalPeriod + 1);
let ema = macdData.slice(0, signalPeriod).reduce((a, b) => a + b, 0) / signalPeriod;
signalLine.push({
time: macdLine[signalPeriod - 1].time,
value: parseFloat(ema.toFixed(2)),
});
const firstMacd = macdLine[signalPeriod - 1];
if (firstMacd) {
signalLine.push({
time: firstMacd.time,
value: parseFloat(ema.toFixed(2)),
});
}
for (let i = signalPeriod; i < macdData.length; i++) {
ema = (macdData[i] - ema) * multiplier + ema;
signalLine.push({
time: macdLine[i].time,
value: parseFloat(ema.toFixed(2)),
});
const macdItem = macdLine[i];
if (macdItem) {
signalLine.push({
time: macdItem.time,
value: parseFloat(ema.toFixed(2)),
});
}
}
}
@ -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)),
});
}

View file

@ -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,
};
});

View file

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