Jimin Huang
add: Feature
5fa7a59
// 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 }
}