Skip to main content

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:
  1. Portfolio-Based Sharpe (preferred): Uses portfolio NAV snapshots to compute annualized risk-adjusted returns
  2. 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.

Performance Metrics Module

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

Build docs developers (and LLMs) love