Playbook
5 worked recipes for common portfolio-math decisions. Each recipe is a complete curl call with interpretation.
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.
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.
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.
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:
- Call
GET /v1/usageto check remaining quota. - If
remaining < 10, skip optimization and hold current positions. - 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;
}