Phasor Intelligence
Portfolio value
Unrealized P&L
Universe coherence
Open positions
Q4 alerts
Proj Q4 risk
Action queue
View market: no markets yet
Market Management — Buy side
run cycle to populate
Portfolio Management — Sell side
run cycle to populate
Portfolio Assessment — five questions on the book itself
run cycle to populate
Buy chain — Q1–Q5
run cycle to populate
Sell chain — Q6–Q10
run cycle to populate
Assessment chain — Q11–Q15
run cycle to populate
Action queue
no pending actions
Activity feed — last 10
no events
View market: no markets yet
Q1. Market management
run cycle
Q2. What to buy — Signal agent
run cycle
Q3. When to buy — Timing agent
run cycle
Q4. How much to buy — Sizing agent
run cycle
Q5. What price to buy at — Price agent
run cycle
View market: no markets yet
Q1. Portfolio management
run cycle
Q2. What to sell — Signal agent
run cycle
Q3. When to sell — Timing agent
run cycle
Q4. How much to sell — Sizing agent
run cycle
Q5. What price to sell at — Price agent
run cycle

Open positions

Portfolio value
Cash
Invested
Unrealized P&L
Realized P&L
Total return
Positions
loading

Closed positions

loading
View market: no markets yet
Coherence Decomposition
run a batch + cycle to populate coherence data
View market: no markets yet
Market Environment
run a cycle to see environment data
Regime Distribution
Sector Leadership
Claude Assessment
loading

Euler's Formula — drag the dot

e = cos θ + i·sin θ  ·  multiply by r → z = r·e

Re = r·cos θ = +0.50
Im = r·sin θ = +0.87
θ = 60° = 1.05 rad
|z| = 1.00
Bars on chart: 0
YOU ARE HERE
Q1: visible up, hidden up — markup with fuel.
What you're seeing. The dot on the unit circle is e. Its shadow on the horizontal axis is cos θ — what we call Re, the visible price momentum a chart shows. Its shadow on the vertical axis is sin θ — what we call Im, the hidden flow pressure that leads price by 90°. Multiply the whole thing by r and the dot moves on a circle of radius r instead.
The price chart below. Every time you move the dot, one new bar is appended to the chart. The green line is Re — the synthetic stock price you are producing right now, exactly what a chart would record. The purple line is Im — the hidden flow, which a chart would never show. Walk slowly all the way around the circle and the green line draws a clean cosine wave; the purple line draws the same wave shifted by 90° (a sine). That's the entire pricing model in one motion: price is the horizontal shadow of a rotating phasor. Wiggle the dot or jump randomly and you'll see noise instead of a clean cycle — which is what real markets often look like up close.
Why this matters historically. Leonhard Euler published this identity in 1748. Richard Feynman called the special case e + 1 = 0 "the most remarkable formula in mathematics." It's used everywhere: AC circuits (Steinmetz, 1893), quantum wave functions, FM radio, MRI reconstruction. Same kernel, different signal — here the signal is price.
Try this.
  1. Set θ = 0°. Re=1, Im=0 — the rightmost point. Pure accumulation: visible neutral, hidden also flat.
  2. Drag to θ = 90°. Re=0, Im=1 — the top of the circle. Pure markup: visible price still flat but hidden flow at maximum. The signal that comes before the chart shows it.
  3. Drag to θ = 180°. Re=−1, Im=0 — the left side. Distribution into markdown.
  4. Drop r to 0.20. The dot now traces a small circle close to origin. That's a quiet, range-bound stock — same θ rotation, much less energy.

Phasor Rotation — click any sector

The Wyckoff cycle as one rotating arrow on a coloured wheel

θ =
Regime = accumulation
Re = +1.00
Im = 0.00
CURRENTLY IN
Accumulation. Smart money quietly buying. Chart looks flat. Hidden pressure rising. The move begins here.
What you're seeing. Six coloured sectors map θ-ranges to Wyckoff stages. Click any sector to jump there. As θ advances counter-clockwise, the stock walks the cycle: accumulation → markup → distribution → markdown → capitulation → re-accumulation → back to accumulation. One full rotation = one full cycle.
The six stages, in plain language.
  • Accumulation (315°–45°) — quiet basing. The chart looks boring; smart money is buying.
  • Markup (45°–135°) — the visible uptrend everyone notices.
  • Distribution (135°–180°) — top forms. Price still rising but Im flips. Q4 warning.
  • Markdown (180°–225°) — visible downtrend. The bill comes due.
  • Capitulation (225°–270°) — panic bottom.
  • Re-accumulation (270°–315°) — base rebuilds. Cycle restarts.
Try this. Press Play with ω=1.0 — the arrow rotates at constant speed, hitting every regime in turn. Real stocks don't move like this; they grind slowly through accumulation and accelerate through markup. Drop ω to 0.2 and the rotation crawls — that's a quiet stock. Push to 3.0 and you're watching a panic-driven mini-cycle compress into days. The instantaneous frequency dθ/dt is exactly this knob.

Hilbert Transform — from one number to two

A noisy 1-D price → a clean 2-D phasor. Scrub bar-by-bar to watch.

Bar t = 60
Price (raw + noise) = +0.30
Re (filtered) = +0.20
Im (Hilbert) = +0.95
θ = 78°, r = 0.97
AT THIS BAR
Re positive and Im positive — Q1, the trend has fuel.
The problem. A chart gives you one number per bar — the closing price. To know where the stock is in its cycle you need two — a real and an imaginary axis. The Hilbert transform manufactures the missing axis from the price alone, by shifting every frequency component of the signal by 90°. That's it. No new data. Just a deterministic mathematical operation.
What the panels show. Top wide panel: noisy price (orange dots), Butterworth-filtered Re (blue), Hilbert-derived Im (purple), with a vertical line marking your scrub position. Right square panel: the same point drawn as a 2-D phasor (Re, Im) with the current θ as an arrow. As you scrub, the phasor on the right walks around the circle.
Try this.
  1. Slide noise to 1.0. The orange dots become jagged but the blue Re curve stays smooth — that's the Butterworth filter. The phasor on the right doesn't wobble.
  2. Slide period to 20. The cycle compresses; you'll see two full rotations of the right-side phasor as you scrub the same range. Faster cycle = stocks that mean-revert quickly.
  3. Slide period to 80. Slow grind. One rotation barely fits. This is what an annual cycle looks like.
  4. Scrub to a point where Re is small but Im is large. The right-side dot is near the top of the circle — that's pure markup. The visible price hasn't moved, but the trajectory shows hidden flow at peak.
Where this came from. David Hilbert defined the transform around 1905. Dennis Gabor (1946) used it to show that any real signal has a unique analytic companion — the same signal with its 90° twin attached. FM radio, sonar echo processing, vibration analysis, MRI k-space all run on it. Applying it to price is structurally identical to demodulating a single-sideband AM signal.

Taylor Extrapolation — projection cone

θ̂(t+k) = θ(t) + ω·k + ½α·k²  ·  the same calculus that flies satellites

H1: θ̂ = 65°
H3: θ̂ = 75°
H5: θ̂ = 85°
H7: θ̂ = 95°
H10: θ̂ = 110°
AT H5 (default trade horizon)
θ̂ = 85° — still in markup. The trajectory says the conviction is intact through your hold horizon.
What you're seeing. Solid white dot = where you are now (θ). Dashed arc = the projected trajectory as time advances. Tick dots on the arc = θ̂ at horizons 1, 3, 5, 7, 10 bars. The grey wedge fanning outward = the confidence cone — narrow when ω has been steady, wide when it's been erratic.
The math, plain English. Term 1 (θ) — where you are. Term 2 (ω·k) — straight-line motion: where you'd be if rotation speed never changes. Term 3 (½α·k²) — the curve: how that speed changes over time. Three terms, smooth signals, useful out to 5–10 bars.
Try this.
  1. α = 0, ω = +5° → straight rotation. H10 lands at θ+50° — clean markup throughout.
  2. α = −0.5° (decelerating). The dashed arc bends back — the move is losing energy. By H10 you've barely advanced past H5. This is when to take partial profits.
  3. α = +0.5° (accelerating). H10 overshoots into late markup / early distribution. The trajectory says the trade is about to outrun itself.
  4. Drop confidence to 0.10. The grey cone fattens dramatically — at this confidence the projection is too noisy to act on, so the engine ignores it and falls back to current-state logic.

KNN Matching — drag the pin to ask "what happened last time?"

Snapshot says where you are. KNN says how it played out historically.

Most likely next regime: distribution (38%)
Median bars to transition: 12
PRECEDENT SAYS
In markup with similar θ and r, distribution followed in 38% of historical cases, with a median of 12 bars before the rollover.
What you're seeing. The big circle is the phasor disk. Grey dots are 1,200 synthetic historical samples generated for this demo. The white pin is your current state — drag the θ and r sliders to move it. Indigo halos light up around the K nearest neighbours. The stacked-bar chart below is the distribution of "next regime" labels among those neighbours.
How fingerprinting works. Each historical bar becomes a vector. In the production system the vector has 5 dimensions — position-in-zone, ω, r, sign(Im), coherence. For this demo we use 2 (θ, r) so you can drag the pin and watch the geometry. The precedent view is the third leg of phase intelligence: snapshot (where you are), trajectory (where you're going), precedent (what came after similar states).
Try this.
  1. Drag the pin into capitulation (around θ=240°, r=0.6). The histogram floods toward "re-accumulation" — capitulation usually leads to base-building, with a median ~15 bars to the next regime.
  2. Drag into late markup (θ=120°, r=0.7). Watch "distribution" jump up. This is the high-conviction exit signal.
  3. Crank K from 50 to 200. The histogram smooths but doesn't shift much — robust regions of the disk give the same answer regardless of how many neighbours you ask.
  4. Crank K to 10 in a sparse region (top edge, r near 0.95). The histogram becomes spiky and unreliable — that's the warning sign of a sparsely-supported state.

Four Situations — drag the pin anywhere

Re × Im → one of four clear actions, no thresholds, no ambiguity.

Re = +0.50
Im = +0.30
Quadrant: Q1 — Re+ Im+
Conviction tag: hl
ACTION
Hold or add. Both visible momentum and hidden flow are positive — the trend has fuel.
The four boxes.
  • Re+ Im+ (Q1) — Hold or add. hl high-conviction long. Both axes agree.
  • Re+ Im− (Q2) — Take profits. dw divergence warning. Price still rising but smart money exiting. This is the Q4 alert in the PM ledger.
  • Re− Im+ (Q3) — Watch for bottom. pr pre-reversal. Price falling but hidden buying. Reversal forming.
  • Re− Im− (Q4) — Wait. he high-conviction exit. No floor — don't catch the falling knife.
Try this.
  1. Start in green (Q1). Slowly drag Im downward across zero. The instant Im flips negative the action changes from "hold" to "take profits" — even though Re hasn't moved. That moment is the Q4 alert. The chart shows nothing.
  2. Drag the pin into Q3 (Re− Im+). This is the early-bottom geometry: the chart is making lower lows, but smart money is stepping in. Most retail traders refuse to look at this quadrant; the framework names it and waits.
  3. Drag into Q4 (Re− Im−). Action: wait. The instinct to "buy the dip" is exactly what the framework refuses — there's no floor here.
  4. Drag along the Im axis (Re=0). Notice the action flips at the zero crossings — Im is the leading indicator, Re is the confirmation.

Portfolio intelligence, ten agents deep

This is the math and the wiring behind IMP Portfolio Manager. The Idiya Market Phasor pipeline gives us a complex number per stock per day. This system extrapolates where each stock is going, trades on the projected conviction — not today's — and uses incoming bars to confirm or exit. Ten decisions, five to enter, five to exit, all recorded with full phasor state.

1 · The one idea

Buying and selling a stock is not one decision. It's five. What to buy, when to buy, how much to buy, at what price, and — underneath all of it — should you be buying at all right now. Selling is the same five in reverse. Ten questions in total, and they are not independent.

But there's a deeper shift. We don't trade on today's state. We trade on where the stock is going. The phasor gives us θ(t) — where the stock is in the Wyckoff cycle right now. From angular velocity and acceleration, we extrapolate θ̂(t+k) — where it will be at the holding horizon. The signal agent filters by projected conviction, not current conviction. Incoming bars after entry either confirm or disprove the trajectory. That's the observe loop: stay or leave.

The reframe: most systems react to what a stock looks like today. IMP Portfolio Manager acts on what the trajectory says it will look like at maturity — then watches the proof arrive bar by bar.

2 · Ten questions, two chains

The system runs two parallel five-question chains every cycle:

Buy chain
Market management
Q1 environment → Q2 signal → Q3 timing → Q4 sizing → Q5 price
Sell chain
Portfolio management
Q1 portfolio env → Q2 signal → Q3 timing → Q4 sizing → Q5 price

Each question is answered by one agent. Each agent has a deterministic pre-filter (math) followed by a curated narrative (Claude). The math decides what's eligible. Claude decides what's worth attention. Order matters: environment gates everything downstream.

3 · The dependency graph

The ten agents are a directed graph, not a flat list. Upstream agents constrain the search space of downstream agents. An agent never runs until its parents have produced output.

Environment ─▶ Signal ─▶ Timing ─▶ Sizing ─▶ Price

Concretely: if the environment agent returns overall_action = neutral, the buy chain halts. If it returns risk_budget < 0.20, the sizing agent is capped at 2% per position regardless of what Kelly suggests. If the signal agent produces an empty list, nothing reaches timing. The graph is enforced in [agents/agent_runner.js](phasor_pm/agents/agent_runner.js), not in the UI, because a UI-only gate is a gate that can be bypassed by calling the API directly.

Why enforce this in code: trader discipline is most often violated in the order of operations — sizing a position before confirming the environment is how portfolios blow up in Chaotic regimes. The dependency graph makes that impossible, not merely discouraged.

4 · The four convictions

Every ticker in the universe is tagged every night with one of four convictions, derived from its phasor state. These come directly from the Market Phasor framework: the visible price move (Re(z)) and the hidden flow pressure (Im(z)).

hl · High-conviction long
Buyable
Visible up, hidden up. Both axes agree. The signal agent's primary input.
dw · Divergence warning
Trim / exit
Visible up, hidden down. Distribution. Never bought. Always reviewed if held.
pr · Pre-reversal
Watchlist
Visible down, hidden up. Bottom forming. Surfaces, never auto-buys.
he · High-conviction exit
Force sell
Visible down, hidden down. No floor. Any open position must be reviewed this cycle.

Conviction is stored with every position entry and every exit. The delta (entry conviction → exit conviction) is the post-hoc explanation for why the trade worked or didn't — recorded automatically, not reconstructed.

5 · Environment gating

Before any individual stock is considered, the environment agent computes a single market-wide number: universe coherence. This is the fraction of tickers whose phasors are rotating in the same direction as their sector median. It answers one question: is the market moving together, or is it fragmented?

env = Trend    if coherence ≥ 0.60
env = Rotation if 0.35 ≤ coherence < 0.60
env = Chaotic  if coherence < 0.35
  • Trend — buy leaders in leading sectors. Risk budget 100%.
  • Rotation — buy only relative leaders. Risk budget 60%. Kelly cap halved.
  • Chaotic — no new entries. Only he exits execute. Risk budget 0%.

The sell chain's environment agent runs a different computation: portfolio-weighted coherence of currently open positions. If that number drops faster than the universe's, the portfolio is decoupling from the market — which usually means the portfolio is wrong, not the market.

6 · Sizing: coherence-damped Kelly

The sizing agent is the one place where math fully overrides Claude. Given an edge estimate p (probability the trade wins) and a payoff ratio b (win:loss), the classic Kelly fraction is:

f* = (b·p − (1 − p)) / b

The framework modifies this in two ways. First, Kelly is clipped to a ceiling (default 0.25 — quarter Kelly) to survive parameter noise in p. Second, it's multiplied by the universe coherence at the moment of the entry, which rises and falls with the environment:

size = min(kelly_cap, f*) · coherence · portfolio_equity

The coherence multiplier is the key. In a Trend regime it barely matters (coherence ≈ 0.7). In a Rotation regime it shrinks positions to ~half their Kelly size. In Chaotic, it goes to zero and no new entry is possible. The same formula that opens positions aggressively in calm markets refuses to open any in loud ones — with no human intervention and no separate "risk-off" switch.

Why this isn't just "Kelly with a risk knob": the coherence number isn't a setting. It's measured, nightly, from the Hilbert transform of every stock in the universe. Sizing is downstream of physics, not of a risk manager's mood.

Smart money management — cash-aware sequential sizing

The Kelly section above tells you how much to size a single position. This section is about everything else: where the cash actually comes from, what stops a strategy from over-drawing, and why the backtest engine refuses to "deploy capital at will."

The problem with naive backtests

A typical phasor-driven backtest sees five new entries in a single day, sizes each one independently against the day's portfolio equity, and "executes" all five — even if their combined notional is twice the available cash. The backtest looks great because every entry was timed. The result is a number that no live system could reproduce, because no live broker would let you buy without the cash to pay for it.

We hit this exact problem early. The Conservative strategy was returning −17% in backtest while the live ledger insisted on −7.8%. The gap was not strategy quality. It was that the backtest had silently financed itself with cash it didn't have.

The reconcile-cash-and-value loop

The fix is to reconcile portfolio cash and mark-to-market value between every trade. The function reconcileCashAndValue(), ported from the live engine into the backtest ledger, walks the open positions and updates:

portfolio_current_value = cash + Σ(position_qty × current_price)
available_cash = cash − Σ(pending_entry_notional)

It runs at three points: before the day's signal pass, before every individual entry, and again after every exit. The sizing agent reads available_cash, not yesterday's snapshot of portfolio_value. A position that would have been allowed under naive sizing is rejected if the cash isn't there.

Sequential cash-aware sizing

When the buy chain produces five candidates in one day, they are sized one at a time, not in parallel. The first candidate sizes against the full available cash. The second sizes against cash after committing to candidate one. The fifth — if it survives — sizes against whatever is left.

This sequential pass has two consequences:

  • Order matters. The signal agent ranks candidates by score before the sizing pass, so the highest-conviction names get the cash first.
  • Late candidates are often dropped entirely (size = 0). This is the correct behaviour: you cannot buy what you cannot afford. The ledger records the rejection with a reason (insufficient_cash), so the backtest log explains its own discipline.

The daily deployment cap

Even with cash discipline, a strategy can over-trade. A bull-market day might surface twenty fresh markup candidates with high projected conviction; sequential sizing would happily commit 80% of NAV in one session. We add a daily deployment cap — a hard ceiling on the fraction of NAV that may be moved into new positions on a single bar.

ProfileDaily deployment capReason
Conservative10% of NAVForces incremental entry. Rebuilds the basket over a week, not a day.
Balanced20% of NAVDefault — captures momentum without aggressive concentration.
Aggressive40% of NAVAllows the strategy to act on a clear environment without artificial slow-walking.

NAV-relative drawdown ceiling

Sizing tells you how much to enter. The drawdown ceiling tells you when to stop entering at all. For each strategy we track:

drawdown = (peak_NAV − current_NAV) / peak_NAV

If drawdown exceeds the per-profile ceiling (10% / 15% / 20%), the strategy stops opening any new positions until NAV recovers above the threshold. Existing positions are still managed by the sell chain — exits fire as normal — but the buy chain is silenced. This isn't a stop-loss on individual trades; it's a circuit-breaker on the strategy itself.

The point: the difference between a backtest that says +12% and a live system that delivers +12% is not "alpha." It is the obsessively boring discipline of accounting — every position, every trade, every day. The reconcile loop, sequential sizing, deployment cap, and drawdown ceiling exist to make the backtest a physical simulation, not a paper exercise.

Three risk-tier strategies — same engine, three personalities

One signal pipeline drives three different strategies — Conservative, Balanced, Aggressive — that share the math but differ in the regimes they will act on, how much they size, and how aggressively they exit. The engine is identical; the personality is configuration.

The shared core

Every profile uses the same:

  • Phasor pipeline (causal columns, regime labels, conviction tags)
  • Environment classifier (TREND / ROTATION / CHAOTIC from coherence decomposition)
  • Position Intelligence per-bar tags (early / mid / late / exhausted)
  • Reconcile loop and sequential sizing engine
  • Live ↔ backtest parity guarantee

What changes between profiles is a single configuration block: which regimes to enter on, what to require for confirmation, how aggressively to size, and when to fold.

Conservative

KnobConservative
Entry regimesaccumulation, re_accumulation only
Environment gateTREND only — no entries during ROTATION or CHAOTIC
TREND-direction filterRequired: pct_bullish > 0.50
PI requirementpi_position_in_zone ∈ {early, mid}
Coherence floor0.65
Kelly cap0.10 per position
Daily deployment cap10% of NAV
Drawdown ceiling10%
Late-markup θ exit110°

The conservative profile is "buy the bottom of the cycle, sell halfway up the markup." It refuses ROTATION environments, which historically saw most of its losers, and it refuses to add to a position that's already late in its zone. Holding period is measured in weeks. Win rate is high; per-trade payoff is modest.

Balanced

KnobBalanced
Entry regimesaccumulation, re_accumulation, markup
Environment gateTREND or ROTATION (both require direction confirmation)
TREND-direction filterRequired in TREND and ROTATION: pct_bullish > 0.50
PI requirementpi_position_in_zone ∈ {early, mid}
Coherence floor0.50
Kelly cap0.20 per position
Daily deployment cap20% of NAV
Drawdown ceiling15%
Late-markup θ exit120°

The default profile. Will participate in markup if the environment confirms the direction, but still refuses bare distribution / markdown setups. The TREND-direction filter is the single most important post-launch fix — without it, the strategy was getting chopped on bullish-looking ROTATION environments where the bullish stocks were a minority.

Aggressive

KnobAggressive
Entry regimesaccumulation, re_accumulation, markup
Environment gateTREND or ROTATION
TREND-direction filterRequired only in ROTATION
PI requirementpi_position_in_zone ∈ {early, mid, late}
Coherence floor0.40
Kelly cap0.40 per position
Daily deployment cap40% of NAV
Drawdown ceiling20%
Late-markup θ exit130°

The aggressive profile will chase late markup, accept lower-clarity environments, and size bigger. Higher win-rate volatility and bigger drawdowns are the price. The drawdown ceiling at 20% is non-negotiable — even the most aggressive profile has an automatic timeout.

The TREND-direction filter — the +2.6% fix

An early version of Balanced silently accepted any TREND environment as bullish-permissive. In practice, TREND just means "stocks are moving together" — they could be moving together down. We added a one-line filter requiring pct_bullish > 0.50 (the fraction of the active universe currently in markup or accumulation must exceed half) before a TREND environment counts as buyable.

Result, on the same backtest window:

ProfileBefore filterAfter filterΔ
Conservative−17.0%−15.3%+1.7%
Balanced−6.5%−3.9%+2.6%
Aggressive+4.1%+6.4%+2.3%

One line of YAML, three strategies improved. The biggest single gain came from the smallest piece of code — a reminder that the strategy lives in the configuration, not in the engine.

7 · Q4 alerts and exits

Q4 is shorthand for the fourth quadrant of the phasor plane — visible up, hidden down — the distribution warning. When a stock that the portfolio holds crosses into Q4, the sell chain fires a Q4 alert. The alert has two effects:

  • The position is immediately queued for review in the sell chain's signal agent, regardless of P&L.
  • The ledger records imag_sign_changed = 1, flagging the crossing for later attribution analysis.

The alert is a review trigger, not an auto-sell. The timing agent still has to confirm that now is the right moment to exit. But once a stock is in Q4, the system will keep asking "sell?" every cycle until it's either out of the quadrant or out of the portfolio.

Wyckoff timing knobs — extended exit logic

The Q4 alert above is one trigger. The full sell chain has three more, each tied to a Wyckoff lifecycle observation, each individually tunable per strategy. Together they let a profile decide how aggressively it folds a position before the price actually breaks.

entry_regimes_allowed

Before any entry, the timing agent checks the candidate's current regime against the strategy's whitelist. Conservative whitelists only {accumulation, re_accumulation}. Aggressive adds markup. None of the profiles whitelist distribution, markdown, or capitulation for entry — those are exit-only states.

A candidate's regime is computed from its phasor on every bar; the gate runs every bar. A markup-only profile that has been holding through a rollover into distribution will not be re-allowed to enter on a subsequent bar at the same price — the gate stays closed until the regime cycles back into a whitelisted state.

late_markup_exit_theta

Markup is buyable. Late markup is not — by the time θ has rotated past 120° or so, the move's energy is bleeding into distribution. We define a per-profile threshold: if any open position's θ exceeds late_markup_exit_theta, the position is queued for exit at the next bar.

ProfileThresholdBehaviour
Conservative110°Sells halfway through markup. Leaves money on the table; rarely sells into distribution.
Balanced120°Sells near the top of markup. Catches more of the move; occasionally sells the early distribution bar.
Aggressive130°Sells deep into markup. Sometimes captures the parabolic blow-off; pays for it with worse exits.

The threshold is a θ value, not a price. A stock with θ=120° at 1,000 IDR and a stock with θ=120° at 10,000 IDR exit at the same point in their cycle, regardless of currency, sector, or absolute price level.

exit_on_projected_distribution

Phase extrapolation projects θ at horizons H1–H10. If the projection at H5 has θ̂ already in distribution territory (> 135°), the position is flagged for exit now, even if today's θ is still in markup. This is the "trade the trajectory" exit — fold while the price is still healthy because the trajectory says it won't be in five bars.

Anticipatory exit — pi_anticipatory_exit

Position Intelligence (described later in this document) computes a per-bar anticipatory-exit flag: the bar is in late-zone of its current regime AND the projected next regime is bearish. When this flag fires, the sell chain treats it as a hard exit, on par with a Q4 alert. This catches the majority of "regime drift" losers that the regime label alone misses, because the label only changes after the rollover, while PI sees the rollover coming.

The combined effect

Layering all four exit triggers — Q4 alert, entry-regime gate, late-markup θ, projected-distribution, anticipatory-exit — produces a sell chain that fires earlier than any single trigger alone. On the strict-Wyckoff Conservative profile, this combined sell chain cut the strategy's loss on the historical replay window from −17% to −6.4%. The buy side hadn't changed at all; the entire improvement came from how positions were closed.

Why exits matter more than entries: a strategy with a great entry rule but a poor exit rule will eventually give back every dollar of edge. A strategy with a mediocre entry rule and a great exit rule will compound. The phasor framework's strength is that it produces exit signals from the same math that produced the entry — there is no separate "stop-loss" model to train, no separate technical indicator to fail. Every exit is grounded in the geometry that prompted the entry.

8 · The phasor-state ledger

Every decision is logged with the full phasor state at the moment it was made: regime, θ, r, Re, Im, coherence, capital_flow, conviction, and the universe-wide environment. Entries, exits, and every Q4 crossing get their own row.

entries(t) → exits(t + k) → Δregime, Δconviction, Δcoherence

This is not a transaction log. It's a state log. A transaction log tells you what was bought and sold. A state log tells you what the market looked like when you made the decision, which is the only thing that lets you answer "was this a bad trade or bad luck?" months later. Attribution becomes a SQL query, not a reconstruction exercise.

Design rule: nothing is ever deleted from the ledger. Positions are closed, not removed. Agent reasonings are preserved verbatim. The ledger is the audit trail for an AI system making financial decisions — and the training data for the next iteration of the same system.

9 · The nightly loop

Every night after the Market Phasor pipeline finishes, the agent runner executes in order:

  1. Environment — computes universe and portfolio coherence, classifies regime, sets risk budget.
  2. Resolve projections — checks pending projections from prior runs. Where the horizon has elapsed, computes residual (actual θ vs projected θ). Flags diverged positions for exit review.
  3. Signal (buy + sell) — pre-filters universe and portfolio. Buy signal now includes projection filter: rejects stocks whose projected conviction at H5 degrades. Sell signal adds "projected conviction degraded" trigger.
  4. Timing — decides which signals are ready to act this bar. Rejects entries where projected θ at H5 overshoots past 130°.
  5. Sizing — Kelly + coherence multiplier, respecting risk budget and per-position caps.
  6. Price — sets limit prices using the bar's volatility envelope.
  7. Ledger write + projection record — every decision is persisted with full state. New positions get a projection entry for the default horizon, so residual tracking starts immediately.

The loop is idempotent: rerunning the same bar produces the same decisions. The randomness comes entirely from upstream market data, not from the agents themselves. This is why the system is auditable and why the ledger is meaningful as training data.

The point: IMP Portfolio Manager is a machine that turns a phasor stream into trades, under explicit regime constraints, with every decision recorded in a format that permits later questioning. It does not predict the market. It disciplines the response to it.

10 · Phase extrapolation — trade on where it's going

The single biggest change to the system: we no longer trade on today's conviction. We trade on the projected conviction at a holding-period horizon.

The core shift

Before extrapolation, the signal agent asked: "what conviction is this stock in today?" Now it asks: "what conviction will this stock be in at my holding horizon?" This changes everything:

Before: trade on t
Buy hl today
Stock is in markup with positive hidden flow right now. Hope it stays there.
After: trade on t+k
Buy hl projected at H5
Stock is projected to still be in markup with conviction in 5 bars. The trajectory is confirmed, not hoped for.

How it works — the Taylor series

Brook Taylor (1715) showed that if you know a function and its derivatives at one point, you can reconstruct its value at nearby points:

f(t+k) = f(t) + f'(t)·k + ½f''(t)·k² + …

First term = where you are. Second = velocity × time. Third = ½ × acceleration × time². More terms = further reach, but for smooth signals even two terms are enough over short horizons. This is the same tool Newton used for orbits, Euler used for differential equations, and every GPS chip uses between satellite fixes.

Every bar the phasor gives us three numbers:

  • θ(t) — the phase angle (position in the Wyckoff cycle)
  • ω = dθ/dt — angular velocity (how many degrees per bar the cycle is advancing)
  • α = d²θ/dt² — angular acceleration (is ω speeding up or slowing down, averaged over 20 bars)

Plugging into Taylor, truncated at second order:

θ̂(t+k) = θ(t) + ω·k + ½α·k²

Example: stock at θ=75°, rotating at ω=6°/bar, decelerating at α=−0.4°/bar². At H5: θ̂ = 75 + 30 − 5 = 100° (still in markup). At H10: θ̂ = 75 + 60 − 20 = 115° (approaching distribution). The signal agent sees this and adjusts accordingly — H5 is safe, H10 is a warning.

The same expansion applies to amplitude r(t) to project move strength. From (θ̂, r̂) we reconstruct (Re, Im) and run the same conviction classifier. The output is a projected conviction — hl, dw, pr, or he — at each horizon.

What changes in the agent chain

  1. Signal agent (buy) — after filtering for today's conviction = hl, applies a secondary projection filter: rejects any stock whose projected conviction at H5 degrades to dw or he. Gives a +5 score bonus to stocks projected to stay hl with high confidence. A stock that looks great today but is projected to enter distribution in 5 bars never reaches the buy list.
  2. Signal agent (sell) — adds "projected conviction degraded" as a new review trigger. Positions held where projected conviction worsens to dw or he are flagged for exit review even if today's conviction is still healthy.
  3. Timing agent — rejects entries where projected θ at H5 would overshoot past 130° (late markup). Today the stock may be at θ=75° (sweet spot), but if the extrapolation says it'll be at 140° in 5 bars, you're buying the tail end of the move.

Confidence gating

Not all projections are equally trustworthy. Confidence is derived from the stability of angular velocity:

confidence = max(0, 1 − std(dθ/dt) / 90°)

A stock rotating at a steady 5°/bar has confidence near 1.0. One wobbling wildly has confidence near 0. The system ignores projections below 0.40 confidence — noisy extrapolations are treated as null, and the agent falls back to current-state logic only.

What this means for the portfolio

  • Fewer false entries: stocks that look great today but are projected to degrade never enter the portfolio. The old system would have bought them and taken a loss.
  • Earlier exits: the sell chain flags positions projected to degrade before the degradation actually happens. You trim while the stock still looks healthy on the surface.
  • Early entries on pre-reversals: a stock in pr (pre-reversal) today that's projected to enter hl in 5 bars is the early entry the old system missed entirely. Today it's "watchlist only." With projection, the timing agent can confirm it.

11 · Observe: residual tracking

Every projection is a hypothesis with an expiry. When the horizon arrives, the system compares:

residual = θ_actual − θ̂_projected   (wrapped to ±180°)

This is the tracking loop — the same observe→act pattern used in Kalman filtering and model-predictive control. The system records a projection for every position at entry, then resolves it when the horizon elapses:

|residual| < 45°
Confirmed
Trajectory is holding. The hypothesis that put you in this trade is intact. Stay.
|residual| > 45°
Diverged
Trajectory broke. Something changed — news, capital rotation, sector shift. Review for exit.

The threshold (45°, one regime boundary width) is configurable via residual_exit_threshold in Config. Diverged positions are flagged in the activity feed and queued for the sell chain's next cycle.

Why this is backtestable by construction

Every bar in the historical per-ticker parquets has θ(t) and dθ/dt. At any past bar t, you can run the extrapolation, compare to actual θ(t+k), and compute the residual. No new data needed. The backtest script (src/backtest_extrapolation.py) does exactly this across the full IDX universe — conviction hit rates, residual distributions, confidence calibration — all from existing data.

The loop: extrapolate → filter by projected conviction + margin → commit capital → observe incoming bars → residual confirms or diverges → stay or leave. The system doesn't predict the market. It projects the trajectory, acts on it, and watches for proof.

12 · Live prices

The phasor pipeline runs nightly on end-of-day closes. But the Positions tab also fetches real-time intraday prices from Yahoo Finance (IDX tickers via the .JK suffix) every time you visit the page. This overlays live market value, P&L, and intraday change on top of the phasor-state view.

The live prices don't affect the phasor math (which runs on daily closes only) or the agent decisions (which run on the nightly snapshot). They're purely for portfolio monitoring — seeing your P&L move in real time while the phasor view tells you the structural story underneath.

14 · Coherence decomposition — finding order in chaos

The full IDX universe (1032 stocks) is always CHAOTIC. The solution: don't average the whole room. Find the group that's in agreement and trade that.

The N=1032 problem

The Kuramoto coherence C = |Σ ek| / N averages 1032 unit vectors. Hundreds of those are illiquid small-caps with tiny amplitude (r < 0.005) — they're not participating in any market move, they're just noise. Including them in the denominator dilutes the signal from the 400-500 stocks that are moving together. The coherence can never get above 0.35 because you're measuring "room consensus" in a room where half the people are asleep.

Leave-one-out decomposition

For each stock k, compute what happens to coherence when you remove it:

chaos_impactk = C(universe without k) − C(universe)

If positive: removing this stock increases coherence. It's fighting the consensus — a chaos source. If negative: removing it decreases coherence. It's contributing to alignment. This is O(N) to compute: subtract one vector from the total sum and recompute the magnitude.

Sector coherence

Chaos doesn't just come from individual stocks. Entire sectors can rotate anti-phase to the market. We compute coherence per sector (intra-sector alignment) and inter-sector coherence (do the sector median phasors agree?):

  • High intra-sector, high inter-sector — everything aligned. TREND.
  • High intra-sector, low inter-sector — sectors are internally coherent but rotating in different directions. ROTATION.
  • Low intra-sector — even within sectors, stocks disagree. CHAOTIC.

Filtered universes

Instead of one coherence number, the environment agent now reports four:

UniverseFilterTypical coherenceEnvironment
Fullnone (all 1032)~0.30CHAOTIC
Activer ≥ 0.005~0.35ROTATION
Sector-alignedθ within 90° of sector median~0.69TREND
Active + Alignedboth filters~0.74TREND

The buy chain gates on the Active + Aligned universe — typically 500-600 stocks, coherent enough for systematic trading. The other 400+ stocks aren't wrong; they're just not part of the current move.

Trading the coherent subset

The signal agent still sees all 1032 stocks in the snapshot. But the environment agent's coherence reading — and therefore the risk budget, Kelly cap, and buy_active gate — is based on the filtered universe. This means:

  • The system trades in TREND environment with full risk budget, even though the "raw" market looks chaotic
  • Individual stock filters (conviction, PI duration, Taylor projection) still apply — filtering the universe doesn't lower the bar for individual entries
  • When even the aligned subset drops below 0.35 (actual market-wide stress), the system correctly stops
The insight: "is the market trending?" is the wrong question when the market is 1032 heterogeneous stocks. The right question is "is there a subset of the market that's trending, and is it large and coherent enough to trade systematically?" Coherence decomposition answers that question mathematically.

13 · Why our realtime is not look-ahead

The offline phasor pipeline uses scipy.signal.filtfilt (forward-backward Butterworth) and a full-series FFT Hilbert transform. Both silently read future data. The realtime view and the extrapolation do not.

The two culprits in the offline pipeline

When the nightly batch computes the phasor for bar t, two functions peek ahead:

  • filtfilt — applies the Butterworth filter forward, then backward. The backward pass at bar t uses bars t+1, t+2, …, N.
  • scipy.signal.hilbert — uses a full-series FFT. Every output sample is a linear combination of every input sample, past and future.

This is standard best practice for offline signal analysis and produces the cleanest phase estimate. But the state it reports for bar t was influenced by data that didn't exist on day t.

The causal invariant

At the very last bar N of any series, there is no future to peek at — the series ends there. So:

reality(prices)[N] ≡ causal(prices[:N+1])[−1]

The offline pipeline and the causal pipeline must produce exactly the same answer at the last bar. This is a mathematical identity, not an approximation. It holds to full floating-point precision.

What the realtime view computes

When you search a ticker in the Realtime tab, the system runs MarketPhasor(prices[:t+1]) for every bar t and keeps only the last row each time. This is the causal trail — at every bar, the phasor state a real-time observer would have computed with only the data available up to that moment:

causal[t] = MarketPhasor(prices[:t+1]).to_dataframe().iloc[−1]

Cost: O(N²) per stock. In practice ~200–400ms for 365 bars. Not the bottleneck.

Why the extrapolation is also look-ahead-free

The projected trail on the Realtime charts is computed from the last bar's state — the one bar where offline and causal are provably identical. The Taylor expansion uses only:

  • θ(N) — the phase at the last bar (causal-safe by the invariant above)
  • ω = dθ/dt at bar N — computed from θ(N) − θ(N−1), both causal-safe
  • α = mean(diff(dθ/dt)) over the last 20 bars — each dθ/dt value is the difference of two consecutive causal-safe thetas

No future data enters at any point. The projection is a function of the present and the recent past, extrapolated forward by calculus. The same Taylor expansion run on the causal trail would produce the same projected trail — because at bar N, they are the same number.

The guarantee: the realtime phasor state, the projected trail, and every trading decision derived from them are computed without any look-ahead bias. The offline batch pipeline does use future data for interior bars (producing a cleaner historical picture), but the edge — today's bar, where decisions are made — is causal by construction.

Position Intelligence — per-bar zone tagging

A regime label tells you what stage a stock is in. Position Intelligence tells you where in that stage it is. A stock can be in markup early, mid, late, or exhausted — and the right action differs sharply between those four sub-zones.

Why the regime label alone isn't enough

Two stocks both labelled "markup" can be in very different states:

  • Stock A entered markup three bars ago. Hidden flow still rising. θ=85°. Early markup — the move is just starting.
  • Stock B has been in markup for thirty bars. Hidden flow rolling over. θ=128°. Late markup — the move is nearly done.

The label says they're the same. The phasor's θ says they're miles apart. Position Intelligence formalises this distinction.

The four zones

Zoneθ range within markupAction
early45° – 80°Strongest entry candidate — full sizing.
mid80° – 110°Acceptable entry — half sizing or skip if late-zone exits are tight.
late110° – 135°No new entries. Existing positions queued for review.
exhausted135° – 180°Anticipatory-exit fires. Position closed regardless of price.

The same four-zone breakdown applies to accumulation, re-accumulation, distribution, markdown, and capitulation — each with its own θ ranges anchored to the regime's typical θ window.

Per-bar causal computation

An early version of PI computed the zone only on the latest bar of the snapshot — useful for live, useless for backtest. The fix was to extend the per-ticker worker to compute PI causally on every bar:

pi_zone[t] = classify(theta_causal[t], regime_causal[t])

The worker re-ran across the universe via extend_causal.js; the full pass took 1.4 minutes. After the extension, the backtest engine sees PI tags on every historical bar, and the entry / exit triggers that depend on them — pi_position_in_zone, pi_anticipatory_exit — fire correctly during replay, not just on today's snapshot.

The PI entry filter

When the buy chain runs, candidates are filtered by their current PI zone. Conservative requires pi_position_in_zone ∈ {early, mid}; Aggressive allows {early, mid, late}. A candidate that today looks great by every other metric but is sitting in exhausted zone is rejected before sizing.

Empirically, on the strict-Wyckoff Conservative profile, the PI entry filter cut the trade count from 97 to 7 — a 93% reduction. The remaining 7 trades had a far higher hit rate. The filter wasn't reducing the strategy's edge; it was eliminating the long tail of marginal entries that diluted the average.

The PI anticipatory-exit trigger

Open positions are scanned every bar. If the position's PI zone moves to exhausted AND the projected next regime is bearish (distribution, markdown, or capitulation), the anticipatory-exit flag fires and the sell chain closes the position at the next bar's open. This catches the regime drift that the label alone misses.

Why this matters more than it looks: "stock is in markup" is a useful sentence. "Stock has been in markup for 28 bars and is now in the exhausted zone with a projected rollover into distribution" is an actionable sentence. Position Intelligence is the per-bar reading that turns the regime label from a tag into a decision.

Live ↔ backtest parity — the deployment guarantee

A strategy that backtests well but lives differently is not a strategy — it's a hope. The PM engine is built around a strict guarantee: the same strategy, on the same data, produces the same trades whether run in backtest or live. Bit-for-bit, every signal, every size, every exit.

The two failure modes parity rules out

Live ↔ backtest divergence has two classic causes:

  • Different code paths. The backtest uses one sizing function; live uses another that "handles edge cases." A bug in either silently skews results.
  • Different inputs. The backtest reads from the snapshot; live reads from the streaming MCP. Subtle field-name or unit differences cause silent mismatch — a position is entered in backtest because regime is a string, but skipped live because the streaming format gave it as an enum integer.

Both failure modes are insidious because nothing crashes. The strategy keeps running. The numbers just don't agree.

The parity construction

The engine's solution is single-source: every decision-making function is defined once, in one file, and called by both the backtest harness and the live runner. There is no backtest_size() and live_size(). There is one size(), and it has no awareness of which harness called it.

ComponentLiveBacktest
Signal agentSame moduleSame module
Timing agentSame moduleSame module
Sizing agentSame moduleSame module
Reconcile loopSame functionSame function
Ledger schemaSame parquetSame parquet
Phasor inputsCausal columns from snapshotCausal columns from per-ticker parquet

The only difference between the two harnesses is the source of the price feed: live reads tomorrow's bar from MCP at market close; backtest reads it from the historical parquet. Everything downstream of the price input is shared code.

The parity test

Every CI run replays the last 30 bars through both harnesses and asserts that the resulting trade ledgers are identical. Not approximately equal — identical. Same tickers, same days, same sizes, same prices. If a single field differs, CI fails and the build is blocked.

This is not a "nice to have" guarantee. It is the entire reason the backtest results above (the −17% → −7.8% Conservative improvement, the +2.6% Balanced fix) are quotable. If the backtest were not bit-for-bit identical to the live engine, those numbers would be marketing copy. With parity in place, they are predictions.

The deployment promise: any strategy whose backtest looks acceptable will run live with the same characteristics. No "live tuning" step, no "execution slippage adjustments" tacked on. The PM engine deploys what it backtests.

Trade-log diagnostics — surfacing causal failure modes

Most quant infrastructure gives you P&L. The PM ledger gives you P&L plus the full phasor state at every entry, every exit, every Q4 crossing, and every PI tag. That data lets a separate diagnostic pipeline answer the only question that matters when a strategy underperforms: why did this happen?

The three failure-mode categories

When a strategy loses money, the causes group into three:

  1. Bad signal. The phasor said "buy" and the price went down. The math was wrong, or the market is non-stationary in a way the model doesn't see.
  2. Bad timing. The signal was correct but the entry / exit fired at the wrong moment. PI was misclassified, or the sequential-sizing pass deferred the entry until the move was gone.
  3. Bad environment. The signal was correct, the timing was correct, but the macro environment moved against the trade. ROTATION turned to CHAOTIC mid-trade; correlated tickers all rolled over together.

Each category has different remedies. Conflating them is how a quant team chases their tail for months.

What the ledger captures per row

FieldMeaning
entry_regime / exit_regimeDid the regime change between entry and exit? If yes, regime drift was the culprit.
entry_pi_zone / exit_pi_zoneDid PI correctly tag the entry as early/mid? If a winner was tagged "exhausted" at entry, PI is mis-calibrated.
entry_environment / exit_environmentDid the macro environment change? TREND→CHAOTIC mid-trade is the canonical environment failure.
pct_bullish_at_entryWas the TREND-direction filter satisfied? If a loser entered with pct_bullish < 0.50 in TREND, the filter was missing or bypassed.
imag_at_entry / imag_at_exitDid hidden flow flip? An entry with imag > 0 that exited with imag < 0 is a textbook distribution-rollover loss.
theta_at_entry / theta_at_exitHow far through the cycle did the trade run? Short θ-distance trades are losers; long θ-distance trades are winners.
residual_at_exitIf a projection was registered, did the realized θ match the projected θ? Large residual = trajectory broke.

The diagnostic queries

Three SQL queries, run automatically on every losing trade, classify the failure:

  1. Regime-drift loser: entry_regime IN (accumulation, markup) AND exit_regime IN (distribution, markdown). Five of six losers in a recent Conservative window matched this — they entered in accumulation and the regime drifted within days. Remedy: tighten entry_regimes_allowed in ROTATION environments.
  2. PI-misclassification loser: entry_pi_zone = 'exhausted' on a position that was nominally early-markup. Remedy: review the PI computation for that ticker class.
  3. Environment-shift loser: entry_environment = 'TREND' AND exit_environment = 'CHAOTIC'. Remedy: tighten coherence floor or shorten holding-period intent.

The pipeline runs nightly against the full ledger, summarises the failure-mode mix per profile, and writes a one-page diagnostic to the activity tab. The next morning, the user sees not just "P&L was −1.2%" but "1.2% loss driven by 5 regime-drift exits, 1 environment shift, 0 PI misclassifications."

Where insights go back into the engine

The diagnostic isn't read once and discarded. Patterns that recur across multiple windows are converted into rule changes:

  • The TREND-direction filter (pct_bullish > 0.50) was added because regime-drift losers in TREND clustered on bars where bullish stocks were a minority.
  • The PI entry filter was tightened on Conservative because the bottom-of-zone entries (PI=late) had a 0% win rate in the regime-drift bucket.
  • The drawdown ceiling was added because environment-shift losers showed multi-position cascade behaviour — the ceiling halts entries before the cascade compounds.
The point: a P&L number is an answer to the wrong question. The right question is "what kind of trade did I just lose?" The state-rich ledger plus the three failure-mode queries make the right question answerable in SQL, not in postmortem.
Honest replay — pick a date, every profile competes side-by-side
The agent chain runs once per trading day from start to end, using only data visible at that day (no look-ahead). Fills happen at T+1 close with slippage and commission deducted. Each profile gets its own isolated ledger and runs in parallel. The report below shows side-by-side how each strategy would have performed.
Profiles to compare
Portfolio risk caps (blank = use profile defaults)
Per-stock stoploss is the dominant loss source in classic — disabling it lets winners cover losers across the basket. The portfolio-level drawdown still caps systemic downside.
Fees & slippage (IDX defaults shown)
Universe filter
Default 252 bars / 30% activity matches the live screener. Lower min_bars to extend backtest range at the cost of weaker phasor signals on shorter histories.
Wyckoff timing (blank = use profile defaults)
Strict Wyckoff: pick "Accumulation + Re-accumulation only" for entries, set late-markup exit at 120°, and enable projected-distribution exit. Together they shift the cycle window earlier and exit before the smart money does.
Churn-control overrides (blank = use profile defaults)
Each profile already sets sensible defaults — leave blank to use them. Fill in to override across all selected profiles.
Earliest causal date:
Risk profile — pick a preset or tune below
Select a profile to auto-fill all risk parameters. Manual edits below override the preset.
Agent configuration
Live run calendar
Registered users
Loading…
Live cycle
Idle. Hit "Run now" to start.
Snapshot freshness