Overview
Autonome provides comprehensive portfolio analytics that track performance, risk exposure, and trading efficiency. All metrics are calculated using shared utility functions to ensure consistency across the UI, prompts, and API responses.
Core Metrics
Sharpe Ratio
Autonome calculates Sharpe ratio using two methods:
Portfolio-Based Sharpe (preferred): Uses portfolio NAV snapshots to compute annualized risk-adjusted returns
Trade-Based Sharpe (simplified): Uses closed trade P&Ls for quick approximation
// src/core/shared/trading/calculations.ts:147
export function calculateSharpeRatioFromPortfolio (
portfolioValues : number [],
periodMinutes : number = 1 ,
) : SharpeRatioResult {
if ( portfolioValues . length < 30 ) {
return {
sharpeRatio: 0 ,
sharpeRatioFormatted: "N/A (insufficient data)" ,
isValid: false ,
reason: "Need at least 30 observations" ,
};
}
// Calculate period returns
const returns : number [] = [];
for ( let i = 1 ; i < portfolioValues . length ; i ++ ) {
const prevValue = portfolioValues [ i - 1 ] ! ;
const currValue = portfolioValues [ i ] ! ;
if ( prevValue > 0 ) {
returns . push (( currValue - prevValue ) / prevValue );
}
}
const meanReturn = mean ( returns );
const stdDev = standardDeviation ( returns , meanReturn );
if ( stdDev === 0 || stdDev < 0.0001 ) {
return {
sharpeRatio: 0 ,
sharpeRatioFormatted: "N/A (low volatility)" ,
isValid: false ,
reason: "Volatility too low for meaningful Sharpe" ,
};
}
// Annualize based on observation period
const periodsPerYear = ( 365 * 24 * 60 ) / periodMinutes ;
const annualizedReturn = ( 1 + meanReturn ) ** periodsPerYear - 1 ;
const annualizedStdDev = stdDev * Math . sqrt ( periodsPerYear );
const sharpeRatio = ( annualizedReturn - RISK_FREE_RATE ) / annualizedStdDev ;
return {
sharpeRatio ,
sharpeRatioFormatted: sharpeRatio . toFixed ( 3 ),
isValid: true ,
};
}
Why Portfolio-Based?
Accounts for unrealized P&L and exposure changes
Captures true portfolio volatility
Consistent with industry-standard Sharpe calculation
Trade-based Sharpe (calculateSharpeRatioFromTrades) is used when portfolio history is unavailable or for quick approximations. It divides mean trade P&L by standard deviation of trade P&Ls.
Drawdown Metrics
// src/core/shared/trading/calculations.ts:73
export function calculateMaxDrawdown ( values : number []) : number {
if ( values . length < 2 ) return 0 ;
let peak = values [ 0 ] ! ;
let maxDd = 0 ;
for ( const v of values ) {
if ( v > peak ) peak = v ;
const dd = (( peak - v ) / peak ) * 100 ;
if ( dd > maxDd ) maxDd = dd ;
}
return maxDd ;
}
export function calculateCurrentDrawdown ( values : number []) : number {
if ( values . length < 1 ) return 0 ;
const peak = Math . max ( ... values );
const current = values [ values . length - 1 ] ! ;
if ( peak <= 0 ) return 0 ;
return (( peak - current ) / peak ) * 100 ;
}
Interpretation :
Max Drawdown : Largest peak-to-trough decline in portfolio value (e.g., 15.2% means portfolio fell 15.2% from its peak)
Current Drawdown : Distance from current value to all-time high (0% = at ATH)
Win Rate & Expectancy
// src/core/shared/trading/calculations.ts:63
export function calculateWinRate ( pnls : number []) : number {
if ( pnls . length === 0 ) return 0 ;
const wins = pnls . filter (( p ) => p > 0 ). length ;
return ( wins / pnls . length ) * 100 ;
}
// src/core/shared/trading/calculations.ts:294
export function calculateExpectancy ( pnls : number []) : number {
if ( pnls . length === 0 ) return 0 ;
const wins = pnls . filter (( p ) => p > 0 );
const losses = pnls . filter (( p ) => p < 0 );
const avgWin = wins . length > 0 ? mean ( wins ) : 0 ;
const avgLoss = losses . length > 0 ? Math . abs ( mean ( losses )) : 0 ;
const winPct = wins . length / pnls . length ;
const lossPct = losses . length / pnls . length ;
return winPct * avgWin - lossPct * avgLoss ;
}
Expectancy Formula : (Win% × Avg Win) - (Loss% × Avg Loss)
Positive expectancy = profitable system long-term
Negative expectancy = system loses money on average per trade
Unrealized P&L
Calculated dynamically using current market prices:
// src/core/shared/trading/calculations.ts:28
export function calculateUnrealizedPnl (
position : PositionForPnL ,
currentPrice : number | null ,
) : number {
if ( currentPrice == null || ! Number . isFinite ( currentPrice )) {
return normalizeNumber ( position . unrealizedPnl ) ?? 0 ;
}
const quantity = position . quantity ?? 0 ;
const notional = normalizeNumber ( position . notional ) ?? 0 ;
if ( quantity === 0 || notional === 0 ) {
return normalizeNumber ( position . unrealizedPnl ) ?? 0 ;
}
// Derive entry price from notional / quantity
const entryPrice = notional / quantity ;
const isLong = position . sign === "LONG" ;
return isLong
? ( currentPrice - entryPrice ) * quantity
: ( entryPrice - currentPrice ) * quantity ;
}
Unrealized P&L falls back to stored value if market price is unavailable. This prevents UI flickering during network issues.
// src/server/features/trading/performanceMetrics.ts:58
export async function calculatePerformanceMetrics (
account : Account ,
currentPortfolioValue : number ,
) : Promise < PerformanceMetrics > {
const [ portfolioHistory , closedTradeRealizedPnl , closedOrders ] = await Promise . all ([
getPortfolioHistory ( account . id ),
getTotalRealizedPnl ( account . id ),
getClosedOrdersByModel ( account . id ),
]);
// Calculate trade stats
const tradeCount = closedOrders . length ;
const pnls = closedOrders
. map (( order ) => parseFloat ( order . realizedPnl ?? "0" ))
. filter (( pnl ) => Number . isFinite ( pnl ));
const winRate = tradeCount > 0
? ` ${ calculateWinRate ( pnls ). toFixed ( 1 ) } %`
: "N/A" ;
// Calculate drawdown from portfolio history
const portfolioValues = portfolioHistory
. map (( h ) => parseFloat ( h . netPortfolio ))
. filter (( v ) => Number . isFinite ( v ));
const currentDrawdown = portfolioValues . length > 0
? ` ${ calculateCurrentDrawdown ( portfolioValues ). toFixed ( 1 ) } %`
: "0.0%" ;
const maxDrawdown = portfolioValues . length > 1
? ` ${ calculateMaxDrawdown ( portfolioValues ). toFixed ( 1 ) } %`
: "0.0%" ;
const sharpeRatio = await calculateTradeSharpe ( account . id );
return {
sharpeRatio ,
totalReturnPercent: ` ${ totalReturn . toFixed ( 2 ) } %` ,
closedTradeRealizedPnl ,
tradeCount ,
winRate ,
currentDrawdown ,
maxDrawdown ,
};
}
Portfolio Snapshot
The PortfolioSnapshot type aggregates all portfolio state:
export interface PortfolioSnapshot {
totalValue : number ; // Equity (cash + unrealized P&L)
availableCash : number ; // Free cash (not allocated to positions)
allocatedCash : number ; // Cash used as margin for open positions
unrealizedPnl : number ; // Sum of unrealized P&L from open positions
realizedPnl : number ; // Cumulative realized P&L from closed trades
totalExposure : number ; // Sum of position notionals (leveraged)
effectiveLeverage : number ; // totalExposure / totalValue
}
Calculation Logic :
// src/server/features/trading/getPortfolio.ts (conceptual)
export async function getPortfolio ( account : Account ) : Promise < PortfolioSnapshot > {
const openOrders = await getOpenOrdersByModel ( account . id );
const totalRealizedPnl = await getTotalRealizedPnl ( account . id );
let allocatedCash = 0 ;
let unrealizedPnl = 0 ;
let totalExposure = 0 ;
for ( const order of openOrders ) {
const quantity = parseFloat ( order . quantity );
const entryPrice = parseFloat ( order . entryPrice );
const leverage = parseFloat ( order . leverage ?? "1" );
const notional = quantity * entryPrice ;
allocatedCash += notional / leverage ; // Margin requirement
totalExposure += notional ; // Leveraged exposure
// Calculate unrealized P&L from current mark price
const markPrice = await getMarkPrice ( order . symbol );
const pnl = order . side === "LONG"
? ( markPrice - entryPrice ) * quantity
: ( entryPrice - markPrice ) * quantity ;
unrealizedPnl += pnl ;
}
const totalValue = INITIAL_CAPITAL + totalRealizedPnl + unrealizedPnl ;
const availableCash = totalValue - allocatedCash ;
const effectiveLeverage = totalValue > 0 ? totalExposure / totalValue : 0 ;
return {
totalValue ,
availableCash ,
allocatedCash ,
unrealizedPnl ,
realizedPnl: totalRealizedPnl ,
totalExposure ,
effectiveLeverage ,
};
}
Exposure & Risk Metrics
export interface ExposureSummary {
totalExposureUsd : number ; // Sum of position notionals
totalRiskUsd : number ; // Sum of stop-loss risk per position
exposureToEquityPct : number ; // totalExposure / portfolioValue * 100
riskToEquityPct : number ; // totalRisk / portfolioValue * 100
}
export interface EnrichedOpenPosition {
symbol : string ;
side : "LONG" | "SHORT" ;
quantity : number ;
entryPrice : number ;
markPrice : number ;
unrealizedPnl : number ;
roe : number ; // Return on equity (unrealizedPnl / margin * 100)
leverage : number ;
notional : number ; // quantity * entryPrice
riskUsd : number ; // Distance to stop loss in USD
exitPlan : PositionExitPlan | null ;
}
Risk Calculation :
// Per-position risk = |entryPrice - stopLoss| * quantity
const riskUsd = Math . abs ( position . entryPrice - position . exitPlan . stop ) * position . quantity ;
Analytics Calculations Module
The analytics module provides advanced statistics:
// src/server/features/analytics/calculations.ts:30
export function calculateOverallStats (
modelId : string ,
modelName : string ,
trades : ClosedTradeData [],
currentAccountValue : number ,
variant ?: string ,
) : OverallStats {
const pnls = trades . map (( t ) => t . realizedPnl );
const totalPnl = calculateTotalPnl ( pnls );
const winRate = calculateWinRate ( pnls );
const sharpeRatio = calculateSharpeRatioFromTrades ( pnls );
const returnPercent = calculateReturnPercent ( currentAccountValue );
const wins = pnls . filter (( p ) => p > 0 );
const losses = pnls . filter (( p ) => p < 0 );
const biggestWin = wins . length > 0 ? Math . max ( ... wins ) : 0 ;
const biggestLoss = losses . length > 0 ? Math . min ( ... losses ) : 0 ;
return {
modelId ,
modelName ,
variant ,
accountValue: currentAccountValue ,
returnPercent ,
totalPnl ,
winRate ,
biggestWin ,
biggestLoss ,
sharpeRatio ,
tradesCount: trades . length ,
};
}
Portfolio History & Downsampling
Portfolio values are recorded every minute in the PortfolioHistory table:
// src/server/db/tradingRepository.ts (conceptual)
export async function recordPortfolioSnapshot (
modelId : string ,
snapshot : PortfolioSnapshot ,
) {
await db . insert ( PortfolioHistory ). values ({
modelId ,
netPortfolio: snapshot . totalValue . toString (),
cash: snapshot . availableCash . toString (),
timestamp: new Date (),
});
}
Downsampling for Charts :
To optimize chart performance, historical data is downsampled based on time range:
// src/server/features/portfolio/retentionService.ts (conceptual)
export const DOWNSAMPLE_CONFIG = {
hourly: { threshold: 7 * 24 * 60 , bucketSize: 60 }, // > 7 days → hourly
daily: { threshold: 30 * 24 * 60 , bucketSize: 1440 }, // > 30 days → daily
};
export function downsampleForChart (
data : PortfolioHistory [],
timeRangeMinutes : number ,
) : PortfolioHistory [] {
if ( timeRangeMinutes <= DOWNSAMPLE_CONFIG . hourly . threshold ) {
return data ; // Raw data (1-minute resolution)
}
const bucketSize = timeRangeMinutes > DOWNSAMPLE_CONFIG . daily . threshold
? DOWNSAMPLE_CONFIG . daily . bucketSize
: DOWNSAMPLE_CONFIG . hourly . bucketSize ;
// Group by time buckets and average
// ...
}
Downsampling is server-side to reduce payload size. Raw data is retained for 7 days, then aggregated into hourly/daily buckets.
Real-Time Updates
Analytics update in real-time via SSE (Server-Sent Events):
// src/server/events/workflowEvents.ts (conceptual)
export async function emitAllDataChanged ( modelId : string ) {
const clients = getSSEClients ( modelId );
for ( const client of clients ) {
client . send ({
event: "allDataChanged" ,
data: JSON . stringify ({ modelId , timestamp: Date . now () }),
});
}
}
Client-Side Invalidation :
// src/hooks/useRealtimeUpdates.ts (conceptual)
const queryClient = useQueryClient ();
useEffect (() => {
const eventSource = new EventSource ( `/api/events?modelId= ${ modelId } ` );
eventSource . addEventListener ( "allDataChanged" , () => {
// Invalidate all trading queries
queryClient . invalidateQueries ({ queryKey: [ "portfolio" ] });
queryClient . invalidateQueries ({ queryKey: [ "positions" ] });
queryClient . invalidateQueries ({ queryKey: [ "analytics" ] });
});
return () => eventSource . close ();
}, [ modelId ]);
Trading API Portfolio history and metrics API
Database Schema Portfolio and Orders table structures
Real-Time Events How SSE keeps the UI synchronized
Analytics API Performance statistics and analytics endpoints