Playbook

5 worked recipes for common portfolio-math decisions. Each recipe is a complete curl call with interpretation.

Recipes
  1. Should I rebalance?
  2. How big a position?
  3. Why is my portfolio under-performing?
  4. What if 2008 happens again?
  5. How do I integrate with my trading bot?

Recipe 1 — Should I rebalance?

Problem: Your portfolio has drifted from its target allocation. You want to know whether drift is large enough to trigger a rebalance.

Approach: POST your current return series to /v1/optimize-portfolio to get HRP-optimal weights. Compare against your current weights. If any asset has drifted more than 5 percentage points from the HRP target, rebalance.

curl -X POST https://api.axistruth.com/v1/optimize-portfolio \
  -H "Authorization: Bearer axt_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "returns": [
      [0.01,  -0.005,  0.002],
      [0.02,   0.010, -0.003],
      [-0.01,  0.005,  0.004],
      [0.005, -0.002,  0.001],
      [0.015,  0.008, -0.002]
    ],
    "assetNames": ["SPY", "AGG", "GLD"]
  }'

Response:

{
  "weights": { "SPY": 0.42, "AGG": 0.38, "GLD": 0.20 },
  "method": "hrp",
  "riskContributions": { "SPY": 0.333, "AGG": 0.333, "GLD": 0.333 }
}

Interpretation: If your current portfolio is SPY: 50%, AGG: 30%, GLD: 20%, SPY has drifted +8 pp above target (42%). That exceeds the 5 pp threshold → rebalance SPY toward 42%. GLD is at target; AGG is +8 pp below target → buy AGG.

Rule of thumb: 5% drift threshold balances rebalancing costs against tracking error. Use a higher threshold (8–10%) if your transaction costs are elevated, or if you're in a tax-sensitive account and want to minimize taxable events.

Recipe 2 — How big a position?

Problem: You have an edge in a specific trade (positive expected return, estimated variance). You want to know the Kelly-optimal position size without blowing up.

Approach: POST to /v1/size-position with your return estimate and variance. Use fraction: 0.25 (quarter-Kelly) as a default conservative starting point.

curl -X POST https://api.axistruth.com/v1/size-position \
  -H "Authorization: Bearer axt_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "expectedReturn": 0.15,
    "variance": 0.09,
    "fraction": 0.25,
    "maxDrawdownFraction": 0.25
  }'

Response:

{
  "kellyOptimal": 1.667,
  "recommended": 0.25,
  "clamped": true,
  "cap": 0.25
}

Interpretation: Full Kelly says 1.667 (167% of capital — leveraged). Quarter-Kelly says 0.417. The maxDrawdownFraction: 0.25 cap clamps the final recommendation to 25% of capital. clamped: true confirms the cap was binding.

Dollar sizing example: if your account is $100,000, recommended position = $25,000 in this trade.

Kelly caveats: The Kelly formula requires accurate expected-return and variance estimates. Overestimating edge leads to over-sizing. Quarter-Kelly (fraction: 0.25) cuts the risk of ruin dramatically at the cost of ~44% of theoretical long-run growth rate (Thorp 1975).

Recipe 3 — Why is my portfolio under-performing?

Problem: Your portfolio lagged a benchmark for 3 consecutive months. You suspect factor-level exposure drift (e.g., unintentional momentum short, value tilt).

Approach: POST your holdings to /v1/factor-score. Examine the scores and ranks. Low momentum score + high lowVol score = you're holding losers with low volatility (a common reversion portfolio). High value score = value tilt is present.

curl -X POST https://api.axistruth.com/v1/factor-score \
  -H "Authorization: Bearer axt_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "assets": [
      {
        "ticker": "XOM",
        "priceHistory": [<253 daily close prices>],
        "roe": 0.22,
        "debtToEquity": 0.35,
        "bookValuePerShare": 28.0,
        "price": 110.0
      },
      {
        "ticker": "MSFT",
        "priceHistory": [<253 daily close prices>],
        "roe": 0.41,
        "debtToEquity": 0.20,
        "bookValuePerShare": 25.0,
        "price": 380.0
      }
    ],
    "factors": ["momentum", "value", "quality"]
  }'

Response:

{
  "scores": {
    "XOM":  { "momentum": 0.71, "value": 0.82, "quality": 0.63 },
    "MSFT": { "momentum": 0.89, "value": 0.21, "quality": 0.94 }
  },
  "ranks": {
    "XOM":  { "momentum": 2, "value": 1, "quality": 2 },
    "MSFT": { "momentum": 1, "value": 2, "quality": 1 }
  }
}

Interpretation: MSFT dominates on momentum and quality (consistent with growth leadership). XOM dominates on value (low P/B). If your benchmark is momentum-weighted, heavy XOM exposure explains underperformance during momentum regimes. The attribution answer: value tilt vs a momentum benchmark.

Recipe 4 — What if 2008 happens again?

Problem: You want to know how your current portfolio would perform under a GFC-level credit and liquidity shock.

Approach: POST to /v1/stress-test with your weights, expected returns, daily volatilities, and correlation structure. Use the GFC_2008 scenario. Interpret max drawdown and CVaR 95.

curl -X POST https://api.axistruth.com/v1/stress-test \
  -H "Authorization: Bearer axt_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "weights":          { "SPY": 0.6, "AGG": 0.3, "GLD": 0.1 },
    "expectedReturns":  { "SPY": 0.0003, "AGG": 0.0002, "GLD": 0.0001 },
    "dailyVols":        { "SPY": 0.012, "AGG": 0.004, "GLD": 0.009 },
    "correlationMatrix": [
      [1.0,  0.2,  0.1],
      [0.2,  1.0, -0.1],
      [0.1, -0.1,  1.0]
    ],
    "assetOrder": ["SPY", "AGG", "GLD"],
    "scenarios":  ["GFC_2008"],
    "simulations": 1000
  }'

Response:

{
  "scenarios": [
    {
      "name": "GFC_2008",
      "durationDays": 127,
      "maxDrawdown": -0.287,
      "var95": -0.172,
      "cvar95": -0.219
    }
  ]
}

Interpretation: Under GFC conditions (127-day shock window), this 60/30/10 portfolio is projected to draw down ~28.7% peak-to-trough. The worst-5% of simulations average a 21.9% loss (CVaR 95). If a 29% drawdown would violate your risk mandate or force you to liquidate at lows, reduce SPY exposure before the shock, not during.

Note on simulations: More simulations reduce variance in the VaR/CVaR estimate. For production risk reporting, use simulations: 5000 or higher. For exploratory analysis, 500 is sufficient.

Recipe 5 — Integrating with a trading bot

Problem: You have an automated trading bot that executes on a schedule. You want to call AxisTruth before each execution cycle to get fresh position targets.

Pattern: Pre-trade optimization call → compare to current positions → compute rebalance trades → execute.

Idempotency and retries

AxisTruth is stateless — the same request body always produces the same response for deterministic endpoints (HRP, Kelly). You can safely retry on network failure without side effects.

Recommended retry strategy:

import time
import requests

def call_with_retry(url, payload, api_key, max_retries=3):
    for attempt in range(max_retries):
        try:
            resp = requests.post(
                url,
                headers={"Authorization": f"Bearer {api_key}"},
                json=payload,
                timeout=10,
            )
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", 60))
                time.sleep(retry_after)
                continue
            resp.raise_for_status()
            return resp.json()
        except requests.RequestException as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # exponential backoff: 1s, 2s, 4s
    raise RuntimeError("Max retries exceeded")

Node.js pattern (TypeScript)

import { setTimeout } from "timers/promises";

async function callAxisTruth<T>(
  endpoint: string,
  payload: unknown,
  apiKey: string,
  maxRetries = 3
): Promise<T> {
  const base = "https://api.axistruth.com";
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fetch(`${base}${endpoint}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (res.status === 429) {
      const retryAfter = parseInt(res.headers.get("Retry-After") ?? "60");
      await setTimeout(retryAfter * 1000);
      continue;
    }

    if (!res.ok) {
      const err = await res.json() as { message: string };
      throw new Error(`AxisTruth ${res.status}: ${err.message}`);
    }

    return res.json() as Promise<T>;
  }
  throw new Error("Max retries exceeded");
}

Quota management in automated workflows

Free tier is 100 calls/month. If your bot runs hourly, that's 720 potential calls/month — well above the free limit. Before each run:

  1. Call GET /v1/usage to check remaining quota.
  2. If remaining < 10, skip optimization and hold current positions.
  3. Log the skip decision for review.
const usage = await callAxisTruth<{used: number; limit: number}>(
  "/v1/usage", undefined, apiKey
);
const remaining = usage.limit - usage.used;
if (remaining < 10) {
  console.log("Quota low; skipping optimization cycle.");
  return;
}
Pro tier recommendation: If your bot runs more than ~3 times/day or you need guaranteed <300ms p95 latency, use Pro tier (50,000 calls/month). Free tier is best-effort latency.