// JS port of get_return.py's core logic. Prices are taken from provided rows (date, price, recommended_action). function sanitizeRows(rows) { const list = (rows || []).filter(r => r && r.date && r.price != null) list.sort((a, b) => (a.date > b.date ? 1 : -1)) return list } export function computeBuyHoldEquity(rows, initial = 100000, fee = 0.0005) { const data = sanitizeRows(rows) if (data.length === 0) return [] const firstPrice = Number(data[0].price) const equity = [] // open with fee let capital = initial * (1 - fee) for (let i = 0; i < data.length; i++) { const price = Number(data[i].price) let value = firstPrice > 0 ? capital * (price / firstPrice) : capital if (i === data.length - 1) value = value * (1 - fee) // closing fee equity.push(Math.max(0, value)) } return equity } export function computeStrategyEquity(rows, initial = 100000, fee = 0.0005, strategy = 'long_short', tradingMode = 'normal') { const data = sanitizeRows(rows) if (data.length === 0) return [] let capital = initial let position = 'FLAT' // 'LONG' | 'SHORT' | 'FLAT' let entryPrice = 0 const series = [] for (let i = 0; i < data.length; i++) { const { price } = data[i] let dailyCapital = capital if (position === 'LONG') dailyCapital = entryPrice ? capital * (price / entryPrice) : capital else if (position === 'SHORT') dailyCapital = entryPrice ? capital * (1 + (entryPrice - price) / entryPrice) : capital const action = String(data[i].recommended_action || 'HOLD').toUpperCase() if (tradingMode === 'normal') { if (action === 'BUY') { if (position === 'FLAT') { position = 'LONG'; entryPrice = price; capital *= (1 - fee); dailyCapital = capital } else if (position === 'SHORT') { const ret = entryPrice ? (entryPrice - price) / entryPrice : 0 capital *= (1 + ret) * (1 - fee) position = 'LONG'; entryPrice = price; capital *= (1 - fee); dailyCapital = capital } } else if (action === 'SELL') { if (position === 'LONG') { const ret = entryPrice ? (price - entryPrice) / entryPrice : 0 capital *= (1 + ret) * (1 - fee); position = 'FLAT'; entryPrice = 0; dailyCapital = capital } else if (position === 'FLAT' && strategy === 'long_short') { position = 'SHORT'; entryPrice = price; capital *= (1 - fee); dailyCapital = capital } } // HOLD keeps position } else { // aggressive: HOLD forces flat, BUY/SELL switch directly if (action === 'HOLD') { if (position === 'LONG') { const ret = entryPrice ? (price - entryPrice) / entryPrice : 0 capital *= (1 + ret) * (1 - fee); position = 'FLAT'; entryPrice = 0; dailyCapital = capital } else if (position === 'SHORT') { const ret = entryPrice ? (entryPrice - price) / entryPrice : 0 capital *= (1 + ret) * (1 - fee); position = 'FLAT'; entryPrice = 0; dailyCapital = capital } } else if (action === 'BUY') { if (position === 'SHORT') { const ret = entryPrice ? (entryPrice - price) / entryPrice : 0 capital *= (1 + ret) * (1 - fee); position = 'FLAT'; entryPrice = 0; dailyCapital = capital } if (position === 'FLAT') { position = 'LONG'; entryPrice = price; capital *= (1 - fee); dailyCapital = capital } } else if (action === 'SELL') { if (position === 'LONG') { const ret = entryPrice ? (price - entryPrice) / entryPrice : 0 capital *= (1 + ret) * (1 - fee); position = 'FLAT'; entryPrice = 0; dailyCapital = capital } if (position === 'FLAT' && strategy === 'long_short') { position = 'SHORT'; entryPrice = price; capital *= (1 - fee); dailyCapital = capital } } } series.push(dailyCapital) if (i === data.length - 1 && position !== 'FLAT') { // force close last day if (position === 'LONG') { const ret = entryPrice ? (data[i].price - entryPrice) / entryPrice : 0; capital *= (1 + ret) * (1 - fee) } else if (position === 'SHORT') { const ret = entryPrice ? (entryPrice - data[i].price) / entryPrice : 0; capital *= (1 + ret) * (1 - fee) } series[series.length - 1] = capital position = 'FLAT'; entryPrice = 0 } } return series } function pctChangeSeries(series) { const pct = [] for (let i = 0; i < series.length; i++) { if (i === 0) pct.push(0) else pct.push(series[i - 1] ? (series[i] - series[i - 1]) / series[i - 1] : 0) } return pct } export function calculateMetricsFromSeries(series, assetType = 'stock') { if (!series || series.length === 0) return { total_return: 0, ann_return: 0, ann_vol: 0, sharpe_ratio: 0, max_drawdown: 0 } const total_return = ((series[series.length - 1] - series[0]) / series[0]) * 100 const daily = pctChangeSeries(series) let annualDays = assetType === 'crypto' ? 365 : 252 let baseReturns = daily if (assetType !== 'crypto') { const eff = daily.filter(v => Math.abs(v) > 1e-12) baseReturns = eff.length > 0 ? eff : daily } const mean = baseReturns.length > 0 ? baseReturns.reduce((a, b) => a + b, 0) / baseReturns.length : 0 const variance = baseReturns.length > 1 ? baseReturns.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / (baseReturns.length - 1) : 0 const std = Math.sqrt(variance) const ann_vol = std * Math.sqrt(annualDays) * 100 const n = baseReturns.length > 0 ? baseReturns.length : daily.length const ann_return = n > 1 ? ((Math.pow(series[series.length - 1] / series[0], annualDays / n) - 1) * 100) : total_return // sharpe (rf=0) const sharpe_ratio = std > 0 ? (mean / std) * Math.sqrt(annualDays) : 0 // max drawdown let peak = -Infinity let maxDd = 0 for (const v of series) { peak = Math.max(peak, v) maxDd = Math.min(maxDd, (v - peak) / peak) } return { total_return, ann_return, ann_vol, sharpe_ratio, max_drawdown: maxDd * 100 } } export function computeWinRate(rows, strategy = 'long_short', tradingMode = 'normal') { const data = sanitizeRows(rows) if (data.length === 0) return { winRate: 0, trades: 0 } let position = 'FLAT' let entryPrice = 0 let wins = 0 let trades = 0 for (let i = 0; i < data.length; i++) { const { price } = data[i] const action = String(data[i].recommended_action || 'HOLD').toUpperCase() if (tradingMode === 'normal') { if (action === 'BUY') { if (position === 'SHORT') { const pnl = entryPrice - price; wins += pnl > 0 ? 1 : 0; trades++; position = 'LONG'; entryPrice = price } else if (position === 'FLAT') { position = 'LONG'; entryPrice = price } } else if (action === 'SELL') { if (position === 'LONG') { const pnl = price - entryPrice; wins += pnl > 0 ? 1 : 0; trades++; position = 'FLAT'; entryPrice = 0 } else if (position === 'FLAT' && strategy === 'long_short') { position = 'SHORT'; entryPrice = price } } } else { if (action === 'HOLD') { if (position === 'LONG') { const pnl = price - entryPrice; wins += pnl > 0 ? 1 : 0; trades++; position = 'FLAT'; entryPrice = 0 } else if (position === 'SHORT') { const pnl = entryPrice - price; wins += pnl > 0 ? 1 : 0; trades++; position = 'FLAT'; entryPrice = 0 } } else if (action === 'BUY') { if (position === 'SHORT') { const pnl = entryPrice - price; wins += pnl > 0 ? 1 : 0; trades++; position = 'FLAT'; entryPrice = 0 } if (position === 'FLAT') { position = 'LONG'; entryPrice = price } } else if (action === 'SELL') { if (position === 'LONG') { const pnl = price - entryPrice; wins += pnl > 0 ? 1 : 0; trades++; position = 'FLAT'; entryPrice = 0 } if (position === 'FLAT' && strategy === 'long_short') { position = 'SHORT'; entryPrice = price } } } } // force close last position if (position !== 'FLAT' && data.length > 0) { const lastPrice = Number(data[data.length - 1].price) if (position === 'LONG') { const pnl = lastPrice - entryPrice wins += pnl > 0 ? 1 : 0 trades++ } else if (position === 'SHORT') { const pnl = entryPrice - lastPrice wins += pnl > 0 ? 1 : 0 trades++ } } const winRate = trades > 0 ? Math.round((wins / trades) * 100) : 0 return { winRate, trades } }