The non-linear trading balance
Why your trading NAV is allowed to be greater than your allocation — and why we keep them in different DBs.
A user’s balance on inite.fund is a tree:
cash (idle)
├─ long allocations[] → PortfolioMeta(allocation, equity) [linear]
└─ trading allocations[] → TradingAccount + AccountSnapshot [non-linear]
cash + nav + margin_used
+ realized_pnl + unrealized_pnl
total = cash + Σ long.equity + Σ trading.nav
Two stacks, one balance. Long allocations are linear: the equity you see is the strategy’s mark-to-market on the cash you put in. Trading is non-linear: NAV diverges from allocation because of PnL, because of margin (you can use the same dollar twice — perp short hedging spot long), because of unrealized swings. By design.
Why two DBs
It’s tempting to fold trading state into the same SQLite file as the
portfolio. We didn’t. Trading carries its own schema (tm_orders,
tm_fills, tm_positions, tm_account_snapshots, tm_capital_flows)
in a separate trading_module.db with its own SQLAlchemy Base.
Two reasons:
- The trading layer existed first as a standalone research tool.
It had its own DB, its own session factory, its own migrations. Folding
it into web/ would’ve meant rewriting trade history serialization for
no real upside. The cost of “two DBs in one app” is small: a second
sessionmakerand a careful capital-flow API. - Blast radius. A bug in the trading engine that corrupts state
stays in
trading_module.db. The portfolio side — which holds idle cash, long allocations, audit log, user accounts — is untouched. We can drop the trading DB and rebuild from the order log; we can’t drop the portfolio DB without losing money trail.
Capital flow primitives
The contract between the two DBs is a small set of atomic helpers in
web/balance.py:
get_user_cash(user_id)— sum ofUserCashLedgerrows.move_to_long(user, strategy, amount)— append a signed ledger row (negative cash) and incrementPortfolioMeta.allocationin a single transaction.move_to_trading(user, strategy_id, amount)— same, but writes aCapitalFlowRowon the trading side too. Two-DB transaction, idempotent onaudit_ref.compute_user_balance(user)— joins both sides and returns the tree.
Each helper takes an optional audit_ref. Pass the same ref twice → no-op
the second time. Network blip retried four times → ledger has one entry.
What “non-linear” means in practice
Bob has $100k cash. Allocates $30k to paper_levels_bob. The trading
engine opens an ETH perp short with 3x notional → margin_used = $9k,
position notional = $27k. Price moves against, unrealized = -$2k.
Bob’s balance:
cash = $70,000
long = $0 (no long allocations yet)
trading[paper_levels_bob]:
initial_alloc = $30,000
cash = $21,000 (locked: $9k margin)
unrealized_pnl = -$2,000
nav = $28,000 (cash + unrealized excluding margin lock)
total = $98,000 (= 70 + 0 + 28)
Note: nav ≠ initial_allocation. Note also: total ≠ Σ allocations. The
math is:
total = cash + Σ long.equity + Σ trading.nav
If you want a “fair” picture you mark trading at NAV (what you’d have if the position closed now), not at allocation. We do that.
Why it matters for the MCP tool
get_my_balance returns the full tree. When a user asks Claude “what’s
my balance” it gets back this exact dict. The skill’s job is to phrase it
right:
- Lead with
total. - Mention cash separately.
- Long allocations: equity + return %.
- Trading accounts: NAV + margin warning if
margin_used / nav > 0.5.
A user who only had a single number (“$98,000”) would miss the fact that $30k of their capital is at-risk in trading — and that $9k of that is locked in margin. Linear math hides that. The tree shows it.
Lessons
- Separate concerns by blast radius, not by table count. Two DBs is fine if they have a thin, atomic contract.
- Idempotency-by-default makes retries safe. Every capital-flow API takes an audit_ref; a re-run with the same ref is a no-op. The MCP layer can be naive about retries; the audit layer can’t.
- A “balance” with margin in it is a tree, not a number. Showing one number is lying. Showing the tree is the only honest UI.
The full schema is in trading/state/models.py if you want to read it.
- 2026-05-11A ledger you can't quietly rewrite
Every capital flow, every approval, every mode change lands in an audit log with a hash chain. Why this matters in trading software, and how to verify integrity in under a minute.
- 2026-04-30Why we deleted A-grade signals
Counter-intuitive: dropping the strongest setups from the intraday book made the sleeve more profitable, more stable, and easier to size. The comparison data and the reasoning we landed on after a year of running A and B side by side.
- 2026-04-19Per-strategy ownership: your kitchen, your keys
We dropped the global trading-user model. Each strategy now reads venue keys from its owner's vault.