Skip to main content

Description

Retrieves historical portfolio net asset value (NAV) over time, grouped by model. Supports date range filtering and automatic downsampling to optimize chart rendering performance. Essential for displaying portfolio performance charts, calculating drawdowns, and analyzing historical returns.

Input Schema

variant
VariantId
Filter portfolio history by model variant. Valid values: "Apex", "Trendsurfer", "Contrarian", "Sovereign". Omit to fetch history from all variants.
startDate
string
ISO 8601 datetime string for the start of the date range (e.g., "2024-01-01T00:00:00.000Z"). Defaults to earliest available data if omitted.
endDate
string
ISO 8601 datetime string for the end of the date range (e.g., "2024-12-31T23:59:59.999Z"). Defaults to current time if omitted.
maxPoints
number
Maximum number of data points to return. Used for downsampling when dataset is large. Must be between 100 and 15,000. Defaults to an appropriate value based on the query (aggregate mode needs more points since data spans multiple model-variant combinations).

TypeScript Type

type PortfolioHistoryInput = {
  variant?: "Apex" | "Trendsurfer" | "Contrarian" | "Sovereign";
  startDate?: string; // ISO 8601 datetime
  endDate?: string;   // ISO 8601 datetime
  maxPoints?: number; // min: 100, max: 15000
};

Output Schema

history
PortfolioSnapshot[]
Array of portfolio value snapshots over time.
resolution
DownsampleResolution
required
Time resolution of the returned data after downsampling. Indicates the time bucket size used:
  • "1m" - 1-minute intervals
  • "5m" - 5-minute intervals
  • "15m" - 15-minute intervals
  • "1h" - 1-hour intervals
  • "4h" - 4-hour intervals
The resolution is automatically determined based on the data range and maxPoints parameter.

TypeScript Type

type DownsampleResolution = "1m" | "5m" | "15m" | "1h" | "4h";

type PortfolioHistoryResponse = {
  history: {
    id: string;
    modelId: string;
    netPortfolio: string;
    createdAt: string;
    updatedAt: string;
    model?: {
      name: string;
      variant?: "Apex" | "Trendsurfer" | "Contrarian" | "Sovereign";
      openRouterModelName?: string;
    };
  }[];
  resolution: DownsampleResolution;
};

Example Usage

Basic Portfolio Chart

import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/server/orpc/client";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';

function PortfolioChart() {
  const { data, isLoading } = useQuery(
    orpc.trading.getPortfolioHistory.queryOptions({
      input: {}
    })
  );

  if (isLoading) return <div>Loading chart...</div>;

  // Transform data for chart
  const chartData = data?.history.map(snapshot => ({
    timestamp: new Date(snapshot.createdAt).getTime(),
    value: parseFloat(snapshot.netPortfolio),
    model: snapshot.model?.name ?? 'Unknown',
  })) ?? [];

  return (
    <div>
      <h3>Portfolio Performance</h3>
      <p>Resolution: {data?.resolution}</p>
      <LineChart width={800} height={400} data={chartData}>
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis 
          dataKey="timestamp" 
          tickFormatter={(ts) => new Date(ts).toLocaleDateString()}
        />
        <YAxis 
          tickFormatter={(value) => `$${value.toLocaleString()}`}
        />
        <Tooltip 
          labelFormatter={(ts) => new Date(ts).toLocaleString()}
          formatter={(value) => [`$${Number(value).toFixed(2)}`, 'Portfolio Value']}
        />
        <Legend />
        <Line type="monotone" dataKey="value" stroke="#8884d8" name="Portfolio" />
      </LineChart>
    </div>
  );
}

Filter by Date Range

import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/server/orpc/client";
import { useState } from "react";

function DateRangeChart() {
  const [startDate] = useState(() => {
    const date = new Date();
    date.setDate(date.getDate() - 7); // Last 7 days
    return date.toISOString();
  });

  const { data } = useQuery(
    orpc.trading.getPortfolioHistory.queryOptions({
      input: {
        startDate,
        endDate: new Date().toISOString(),
        maxPoints: 500,
      }
    })
  );

  const totalReturn = data?.history.length ? 
    parseFloat(data.history[data.history.length - 1].netPortfolio) - parseFloat(data.history[0].netPortfolio)
    : 0;

  const returnPct = data?.history.length ?
    (totalReturn / parseFloat(data.history[0].netPortfolio)) * 100
    : 0;

  return (
    <div>
      <h3>Last 7 Days Performance</h3>
      <p>Resolution: {data?.resolution}</p>
      <p style={{ color: totalReturn >= 0 ? 'green' : 'red' }}>
        Return: ${totalReturn.toFixed(2)} ({returnPct >= 0 ? '+' : ''}{returnPct.toFixed(2)}%)
      </p>
      {/* Chart component */}
    </div>
  );
}

Filter by Variant

import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/server/orpc/client";

function ApexPerformance() {
  const { data } = useQuery(
    orpc.trading.getPortfolioHistory.queryOptions({
      input: {
        variant: "Apex",
        maxPoints: 1000,
      }
    })
  );

  // Group by model for multi-line chart
  const seriesData = data?.history.reduce((acc, snapshot) => {
    const modelName = snapshot.model?.name ?? 'Unknown';
    if (!acc[modelName]) acc[modelName] = [];
    acc[modelName].push({
      timestamp: snapshot.createdAt,
      value: parseFloat(snapshot.netPortfolio),
    });
    return acc;
  }, {} as Record<string, Array<{ timestamp: string; value: number }>>);

  return (
    <div>
      <h3>Apex Variant Performance</h3>
      <p>Resolution: {data?.resolution}</p>
      <p>Models tracked: {Object.keys(seriesData ?? {}).length}</p>
      {/* Multi-line chart showing each Apex model */}
    </div>
  );
}

Calculate Performance Metrics

import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/server/orpc/client";

function PerformanceMetrics() {
  const { data } = useQuery(
    orpc.trading.getPortfolioHistory.queryOptions({
      input: { maxPoints: 5000 }
    })
  );

  if (!data?.history.length) return <div>No data available</div>;

  const values = data.history.map(s => parseFloat(s.netPortfolio));
  const initialValue = values[0];
  const currentValue = values[values.length - 1];
  const totalReturn = currentValue - initialValue;
  const returnPct = (totalReturn / initialValue) * 100;

  // Calculate max drawdown
  let peak = values[0];
  let maxDrawdown = 0;
  values.forEach(value => {
    if (value > peak) peak = value;
    const drawdown = ((value - peak) / peak) * 100;
    if (drawdown < maxDrawdown) maxDrawdown = drawdown;
  });

  // Calculate daily returns for Sharpe ratio
  const dailyReturns: number[] = [];
  for (let i = 1; i < values.length; i++) {
    const ret = (values[i] - values[i - 1]) / values[i - 1];
    dailyReturns.push(ret);
  }

  const avgReturn = dailyReturns.reduce((sum, r) => sum + r, 0) / dailyReturns.length;
  const variance = dailyReturns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / dailyReturns.length;
  const stdDev = Math.sqrt(variance);
  const sharpe = stdDev !== 0 ? (avgReturn / stdDev) * Math.sqrt(252) : 0; // Annualized

  return (
    <div>
      <h3>Performance Metrics</h3>
      <p>Resolution: {data.resolution}</p>
      <p>Data Points: {data.history.length}</p>
      <p>Initial Value: ${initialValue.toFixed(2)}</p>
      <p>Current Value: ${currentValue.toFixed(2)}</p>
      <p style={{ color: totalReturn >= 0 ? 'green' : 'red' }}>
        Total Return: ${totalReturn.toFixed(2)} ({returnPct >= 0 ? '+' : ''}{returnPct.toFixed(2)}%)
      </p>
      <p>Max Drawdown: {maxDrawdown.toFixed(2)}%</p>
      <p>Sharpe Ratio: {sharpe.toFixed(2)}</p>
    </div>
  );
}

Compare Multiple Variants

import { useQueries } from "@tanstack/react-query";
import { orpc } from "@/server/orpc/client";

function VariantComparison() {
  const variants = ["Apex", "Trendsurfer", "Contrarian", "Sovereign"] as const;

  const queries = useQueries({
    queries: variants.map(variant => 
      orpc.trading.getPortfolioHistory.queryOptions({
        input: { variant, maxPoints: 1000 }
      })
    ),
  });

  const isLoading = queries.some(q => q.isLoading);
  if (isLoading) return <div>Loading comparison...</div>;

  return (
    <div>
      <h3>Variant Performance Comparison</h3>
      {variants.map((variant, idx) => {
        const data = queries[idx].data;
        if (!data?.history.length) return null;

        const initialValue = parseFloat(data.history[0].netPortfolio);
        const currentValue = parseFloat(data.history[data.history.length - 1].netPortfolio);
        const returnPct = ((currentValue - initialValue) / initialValue) * 100;

        return (
          <div key={variant}>
            <h4>{variant}</h4>
            <p>Resolution: {data.resolution}</p>
            <p>Return: <span style={{ color: returnPct >= 0 ? 'green' : 'red' }}>
              {returnPct >= 0 ? '+' : ''}{returnPct.toFixed(2)}%
            </span></p>
          </div>
        );
      })}
    </div>
  );
}

Implementation Notes

  • Precision: netPortfolio is stored as TEXT in the database to preserve decimal precision. Always use parseFloat() for calculations.
  • Downsampling: Server automatically downsamples based on data range and maxPoints to optimize performance. The resolution field indicates the time bucket size used.
  • Retention Policy: Raw data is retained for 7 days, then aggregated into hourly buckets for 30 days. Older data is aggregated into daily buckets.
  • Aggregate Mode: When querying without a variant, data spans all model-variant combinations, requiring higher maxPoints for sufficient granularity.
  • Error Handling: Throws an error on fetch failure with the original error message.

Build docs developers (and LLMs) love