himmi

Chanakya v2: A 17-Agent Portfolio and Wealth Research Platform

11 min read
engineeringdeep-divepythontypescriptllm-agentsfastapichanakyaquantitative-finance

Chanakya is a private research platform built around a real, personally-traded portfolio across the US and Indian markets. Seventeen specialized agents — news, sentiment, technicals, fundamentals, macro, a Bull/Bear/Judge debate, forecasting, risk, weekly allocation, and more — reason across everything at once. Underneath the market-intelligence layer sits a full personal-finance system: a double-entry ledger, tax-lot accounting, and net-worth tracking. The system is advisory only. It proposes and explains; a human decides.

Four months of daily development since the platform's first public write-up have taken it from thirteen agents to seventeen, and from a single deterministic forecasting model to a dedicated quant-research module built around techniques used in institutional portfolio management: dynamic correlation modeling, portfolio optimization, and a full battery of overfitting controls borrowed from published quantitative finance research.

Rendering chart…
Chanakya, by the numbers
Four months of daily development on a single-operator system: commits, schema migrations, and test coverage across the Python backend and TypeScript frontend.

This is a private, personal system with no public repository. What follows is architecture, math, and engineering detail — the shape of the system and how it reasons — not the specific signals, thresholds, or weights that make up its actual trading edge.

Architecture

LayerDetail
LLMOpenAI as the primary provider, with Gemini configured as an automatic fallback if a call fails
Agents17, each an independent Kafka consumer group — News, Sentiment, Technical, Fundamentals, Macro, Events, Ontology, Synthesis, Debate, Forecast, Decision, Risk, Allocation, Journal, Discovery, Grading, and one more
ForecastingA single context-synthesizing LLM call per symbol per horizon, graded by a multiclass correlation metric that can't be flattered by a lazy predictor
Decision compositionNine weighted signals, composed by deterministic code
ExecutionAdvisory only — every real trade is entered into a database-enforced double-entry ledger with full tax-lot accounting
Data collectorsEight independent sources: SEC EDGAR, yfinance (US + India), FRED, RSS/StockTwits, options chains, insider Form 4, 8-K transcripts
ObservabilityOpenTelemetry, Prometheus, Grafana, and Arize Phoenix for LLM-native tracing — prompt, completion, and cost visibility per call
DeploymentEntirely local. Never hosted, never public, by design
Rendering diagram…

Every agent runs as its own restartable Kafka consumer, subscribed to whatever topic feeds it. There is no orchestrator holding the pipeline together — the pipeline is an emergent property of topic names, which means any agent can be redeployed or scaled without the others noticing.

A financial record that can't go out of balance

Every real trade is entered by hand into a double-entry ledger, enforced at the database level: a Postgres trigger guarantees that every transaction's entries sum to zero. Tax-lot accounting sits on top of it — FIFO, HIFO, LIFO, or specific-lot identification, with anniversary-rule long/short-term determination and Form 8949 export. Net worth, cashflow, and bank-import matching round out a full personal-finance surface alongside the market-intelligence agents.

Keeping the financial record accurate matters more than any single trading signal. A broken balance is the one failure mode that actually costs something, and the database-level invariant catches every write automatically, without depending on a person remembering to review it.

Forecasting

A single LLM call synthesizes the full context for a symbol — fundamentals, peer comparables, technicals, macro regime, and time-bounded news — into a three-class directional call (up, down, or flat) per horizon. The "flat" class is volatility-scaled per symbol and horizon rather than a fixed percentage band:

band(s,h)=clamp ⁣(kσ%(s)h,  bmin,bmax)\text{band}(s, h) = \operatorname{clamp}\!\Big(k \cdot \sigma_{\%}(s) \cdot \sqrt{h},\; b_{\min},\, b_{\max}\Big)

where σ%(s)\sigma_\%(s) is a volatility estimate for symbol ss (from ATR or a GARCH(1,1) fit), hh is the horizon in days, and kk, bminb_{\min}, bmaxb_{\max} are fixed shaping constants. A volatile small-cap gets a wider "flat" band than a stable large-cap, and grading against the eventual close uses the same band the forecast was made against.

Grading uses a multiclass generalization of the Matthews correlation coefficient rather than raw accuracy. The two-class version:

MCCbinary=TPTNFPFN(TP+FP)(TP+FN)(TN+FP)(TN+FN)MCC_{\text{binary}} = \frac{TP \cdot TN - FP \cdot FN}{\sqrt{(TP+FP)(TP+FN)(TN+FP)(TN+FN)}}

ranges from 1-1 to +1+1 and lands at exactly 00 for a classifier that always predicts the majority class. Gorodkin's 2001 generalization extends the same idea — correlation between predicted and actual class assignment — across a full confusion matrix, which is what makes it a meaningful bar for a three-class forecaster to clear.

A leakage guard on the underlying data means a backtested or replayed decision only ever sees news published before its own decision timestamp, verified by a dedicated test that plants future-dated articles and confirms they never enter the context window.

The quant research module

A dedicated module handles risk modeling, portfolio construction, and validation — separate from the forecasting agent, feeding the risk and allocation agents directly.

Correlation and covariance. An Engle (2002) two-step DCC-GARCH model, with the Aielli (2013) bias correction, gives a dynamic, time-varying correlation estimate rather than a single static snapshot:

Ht=DtRtDt,Rt=diag(Qt)1/2Qtdiag(Qt)1/2H_t = D_t R_t D_t, \qquad R_t = \operatorname{diag}(Q_t)^{-1/2}\, Q_t \,\operatorname{diag}(Q_t)^{-1/2}

DtD_t is the diagonal matrix of per-asset volatilities from univariate GARCH(1,1) fits; QtQ_t updates each step from an exponentially-weighted mix of standardized-residual cross-products and its own lag. A Marchenko–Pastur-style random-matrix-theory cleaning pass sits underneath it, denoising the sample covariance matrix before it reaches an optimizer — sample covariance estimated from noisy return series is itself mostly noise past a certain eigenvalue rank.

Position sizing. A multivariate fractional-Kelly sizer, with a ridge-stabilized covariance inverse and a conservative fractional multiplier, sets position size from the classical Kelly fraction:

f=bpqb=pqbf^{*} = \frac{bp - q}{b} = p - \frac{q}{b}

pp the probability of a favorable outcome, q=1pq = 1-p, bb the net odds. Portfolio-level construction runs alongside it: Black-Litterman (a market-implied equilibrium prior, tilted by the forecast model's own calibrated views), Hierarchical Risk Parity, and minimum-variance optimization, via PyPortfolioOpt and riskfolio-lib, wired to a live "suggested rebalance" endpoint.

Validation. Purged walk-forward cross-validation keeps training and test windows from leaking into each other through overlapping labels. The Probabilistic Sharpe Ratio converts a point-estimate Sharpe ratio into the probability it genuinely exceeds a benchmark, given a finite sample:

Z=(SR^SR)n1σ^SR,PSR^=Φ(Z)Z = \frac{(\widehat{SR} - SR^{*})\sqrt{n-1}}{\hat\sigma_{SR}}, \qquad \widehat{PSR} = \Phi(Z)

The Deflated Sharpe Ratio applies the same idea after correcting for how many strategy variants were compared before one was selected — the more trials, the higher the bar a real Sharpe ratio has to clear before it reads as skill rather than selection bias. Rounding out the module: cross-sectional factor tools (rank normalization, winsorization, sector/beta neutralization), a Fama-French-style return attribution splitting realized returns into factor premia versus residual alpha, a Corwin-Schultz bid-ask spread estimator that works from OHLC bars alone, and SHAP explainability over the prediction model's gradient-boosted trees.

Every backtest number is cross-checked against two independent, well-known third-party libraries before it's trusted, a standing correctness check on the analytics layer itself.

Tail risk and portfolio limits

Parametric Value-at-Risk and per-position risk-contribution functions feed the risk agent alongside a correlation veto and a drawdown gate that halves position sizing at a 10% portfolio drawdown and blocks new risk entirely at 15%. The parametric VaR form:

VaRα=μzασ\text{VaR}_{\alpha} = \mu - z_{\alpha}\,\sigma

is a normal-distribution estimate of the loss threshold expected to be exceeded only (1α)(1-\alpha) of the time. It sits next to a historical, distribution-free VaR estimate rather than standing alone — two independent estimates of the same risk carry more information than one confident one.

Position and sector limits follow a configurable Investing Policy — risk tolerance, position and sector bounds, time horizon — set once and revisable at any time, rather than a constant buried in application code.

Rendering diagram…

Named Philosophy presets apply pre-built weighting stances to the same regime-by-agent Bayesian weight matrix that composes signal weights across five market regimes. A zero-cost historical replay engine answers "what would this policy have done historically" by replaying the deterministic parts of the pipeline against stored signal history, with no LLM calls involved. A change-preview step shows a policy edit's historical impact before it's committed, and automated sentinel checks watch for policy drift, a weight-tuner that's stopped moving, or a widening calibration gap. Four feedback loops run continuously: a weekly model-promotion gate, an A–F calibration scorecard, the regime-by-agent weight matrix itself, and a classifier trained on which human overrides turned out to be correct.

A walk-forward strategy tuner searches allocation and strategy parameters across a multi-year walk-forward split, with a promotion gate structurally identical to the model-registry gate — a parameter change only goes live if it beats the incumbent out of sample.

Cost and inference discipline

Chanakya runs on a twice-daily pre-market sweep rather than continuous polling, with an input-hash dedup layer that calls a model at most once per (agent, input) pair per day, and a global daily budget cap enforced in micro-dollar precision. A kill switch sits in front of every LLM-calling path, checked before any call goes out.

Reasoning and access control

Every reasoning transcript, decision, and mutating endpoint requires an authenticated session. CSRF validation fails closed if its Redis-backed token store becomes unreachable — a request without a verifiable token is rejected outright rather than waved through. The Kafka administration console, along with the LLM-tracing UI, is bound to loopback only.

Stack at a glance

BackendPython 3.12, FastAPI, SQLAlchemy 2 (async), Alembic — 122 migrations
Data layerTimescaleDB (hypertables + compression), pgvector, a double-entry ledger with a Postgres-enforced balance invariant
Event busKafka via Redpanda — 17 agents, each an independent consumer group
LLMOpenAI as primary, Gemini as automatic fallback, a global kill switch in front of every call
Quant moduleDCC-GARCH, RMT covariance cleaning, VaR/CVaR, Black-Litterman / HRP / min-variance optimization, purged walk-forward validation, PSR/DSR, an independent third-party backtest cross-check
FrontendNext.js 15, TypeScript, ECharts, surfaces for cashflow, research chat, and decision-reasoning transcripts
ExecutionAdvisory only — every trade is entered by hand into the ledger
Tests4,139 backend (pytest), 1,067 frontend (vitest)
CIGitHub Actions — ruff, pyright, biome, tsc, pytest, vitest, plus an AST-based architectural-convention gate
ObservabilityOpenTelemetry, Prometheus, Grafana, Arize Phoenix
DeploymentEntirely local
LicenseProprietary, personal use only

More deep dives in this series: the original Chanakya post for the debate mechanism and the deterministic decision/risk split, and SuperZen and BroSki for the same instinct at a smaller scale — one disciplined mechanism doing the work of a dozen special cases.