Spaces:
Running
Running
| // 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 } | |
| } | |