Temper
A reinforcement-learning execution agent, graded against Almgren–Chriss closed forms it is never allowed to quietly beat. The agent works a parent order in a simulated market; an independently derived oracle says what optimal is, and every threshold is written down before training starts. In the linearised world it rediscovers the analytic schedule to four decimal places; in the world with the real 0.6-power impact law — where the closed form is derived at a tangent and is genuinely wrong — it captures 99.4 % of the difference against a certified optimum. Given an invented one-step-ahead price signal explaining one part in ten thousand of the next bin's return variance, it captures 93 % of the available advantage while paying 1.05× the optimum's own execution premium — three numbers that are only reported together. The same agent then worked a real order on a live matching engine over a wire, as that venue's third independent client — where the number it reports was computed in closed form before anything was sent. Python, PyTorch, single-file PPO, CPU only.
// Overview
Temper trains a reinforcement-learning agent to work a parent order — to sell a large position over a trading day without moving the price against itself more than necessary — and grades it against the Almgren–Chriss closed forms that FrontierView, another project in this portfolio, already implements. It is the differential-oracle pattern from Anvil applied to RL: an independently derived reference engine says what the right answer is, so the agent's score is never a matter of opinion.
The discipline is the point. Under the exact assumptions Almgren–Chriss makes, the closed form is optimal, so "the agent beats Almgren–Chriss" is a red flag rather than a result — it means something in the harness is wrong. Phase 1 therefore claims only that the agent independently converges to the analytic schedule within a tolerance written down before training started. Every milestone is specified in a brief that fixes ε, the seed count and the confidence level in advance; every reported number regenerates from a committed config and seed; every chart carries TWAP and the analytic schedule as baselines and shows the spread across seeds rather than the best run.
That discipline has caught things. The vendored κ turned out to be a continuum-limit expression that drops a factor at realistic order sizes — the two rates differ by about 2.2×, both produce sinh-shaped schedules, and the difference is invisible by eye. Grading against it would have shown a correctly trained agent scoring up to 18 % better than "optimal", which reads exactly like the red flag above. The oracle now carries both conventions, and the honest reading of the finding is narrow: the risk-aversion dial is mislabelled by about 5× at usable settings, not that Almgren–Chriss is wrong. The vendored schedule is the exactly optimal schedule for a different risk aversion, and its point sits on the efficient frontier, never inside it.
The second finding was about noise rather than finance. At the reference case the quantity being optimised is ~2.4 basis points while a single episode's cost varies by ~95, and at that signal-to-noise ratio training is a lottery rather than a slow climb: five seeds scored 0.066, 0.009, 0.819, 0.098 and 0.147 of the available gap — one essentially rediscovering the analytic schedule, another barely learning at all. The same agent with the same hyperparameters, trained on a reward whose noise is cancelled by an exact identity the simulator publishes, clears the bar with a spread orders of magnitude tighter. Both runs are committed and differ in one config field, which is what makes "the estimator closed the gap" a measurement rather than a story. A related discovery, now a portfolio-wide rule: the same seed scored 0.165 and 0.066 on four threads versus eight, because multithreaded floating-point reductions sum in a thread-dependent order — so thread count is a committed input, not a property of the host.
Phase 2 is where the agent is allowed to win, and it wins against something specific. FrontierView's temporary impact is a 0.6-power law; Almgren–Chriss has no closed form for that, so the library linearises at a tangent and solves the linear problem instead. Making the power law the actual world means the closed form is now solving the wrong problem — by 1.54 % of the objective at the reference case, which is 16 878 shares of a 100 000-share order in trajectory space. That world's own optimum has no formula, so it is solved: a Newton iteration on the KKT system, certified by a positive-definite Hessian, a relative residual of 1.2e-15, 3 600 perturbations that all go uphill, and a completely independent bisection solver agreeing to 3.1e-15 of the parent order. Ten seeds captured a median 99.4 % of the available difference, worst seed 99.0 %, landing 727 shares from the certified optimum where the closed form sits 16 878 away.
The second of Phase 2's three parts is the first advantage in the project that no fixed schedule can capture at all. Liquidity becomes a second, independent noise source and the agent gets to see it, so what has to be beaten is no longer a mis-specified formula but the best possible fixed schedule that knows the liquidity law — 2.49661 basis points against the 2.43449 of an optimum over reacting policies, an adaptive advantage of 0.06212 bps and 2.55 % of the objective, under a per-bin liquidity multiplier that is Temper's own invention with one parameter, σ_L = 0.5. That gap is the denominator and not the distance from M4a's schedule, because 0.00234 bps of the latter is a level shift any static solver picks up for free. Ten seeds captured a median 99.0 % of it, worst seed 96.3 %, landing 0.00064 bps above a reference that is converged and bracketed rather than certified — a stochastic dynamic program has no certificate to offer, and that word stays off it. The control is the claim rather than an appendix: re-graded with the observed liquidity drawn independently of the liquidity actually charged, the same policies score −1.01 against a bar of 0.15 fixed before the run, about one advantage-unit worse than the best static schedule. Reacting to a decorrelated signal costs almost exactly what reacting to the true signal gains, and the gap between +0.99 and −1.01 is what says the agent is using the signal rather than having found a better fixed schedule.
The third part is the one whose headline is not a number at all. M5 puts a weak one-step-ahead price signal in the observation and asks the agent to tilt the schedule for it — a signal that is Temper's own invention with one parameter, rho = 0.01, which is to say it explains one part in ten thousand of the next bin's return variance. Predicting the price has to be paid for: tilting to monetise a signal costs execution quality, and the converged optimum itself gives back 45.5 % of the gross effect in order to do it, so a single capture fraction cannot separate a policy that traded the signal well from one that traded it badly and executed well. The result is therefore three numbers, reported together and never one of them alone, each against a bar fixed before the run — alpha capture 0.9767, of the 0.14808 basis points of gross alpha available; execution premium 1.0480x, of the 0.06732 basis points the optimum itself pays; and net capture 0.9341, which is 0.00532 basis points over that optimum on an advantage of 0.08076. Ten seeds, worst seed 0.8925. The denominator is the net advantage rather than the gross alpha, which would have read 1.83x larger in the flattering direction, and the control is the claim here as it was for liquidity: re-graded with the observed signal shuffled, the same policies score a median −0.87 against a bar of −0.50 stated in advance. The reference is a dynamic program again, converged rather than certified — but with a certified floor underneath the half of the objective the signal cannot touch, because impact and risk are convex and carry no signal, so M4a's certified optimum bounds them for any policy whatever. That makes the red-flag test a proof rather than a threshold.
The two ends of the project belong beside each other, because it is the same agent at the same budget both times: at a 1:70 per-episode signal-to-noise ratio on the realised reward, rediscovering a closed form was a lottery, and a signal seven hundred times weaker than that one was recovered almost completely once the reward was the conditional grade and its conditional variance was exactly zero. It fails on a large signal under noise and succeeds on a tiny one without it, so across both phases the binding constraint was the estimator's variance rather than the signal's strength.
M4a's methodological finding is worth more than its result, and both milestones after it repeated the lesson in a new form. The tolerance every earlier milestone used — 5 % of the distance TWAP covers — is 1.8× the entire effect being measured, so an agent capturing none of the mis-specification would have passed it comfortably. A tolerance is only portable within the question it was written for. M5 took the same idea one step further: when the advantage is a difference of two larger numbers that move together, no single fraction is portable either, and the headline has to be the whole decomposition.
What none of this establishes is real-market performance. Every number here is a statement about a simulator whose dynamics are analytic and whose parameters are a calibrated synthetic set, with two exceptions named above: the liquidity process and the alpha signal are both Temper's own inventions, calibrated against nothing, so the 2.55 % that seeing liquidity is worth and the 0.081 bps the signal is worth are statements about those models rather than about a market. There are no real fills and no historical order-book data anywhere in the project. The differential tests are what make it a good simulator, and the honest reading of the results is "the machinery is correct", not "the strategy would make money".
The per-commit suite runs from a clean clone in about two minutes ten, against a three-minute ceiling every brief has stated in advance; the deep Monte-Carlo tiers and the multi-hour acceptance runs sit behind markers and drivers so the commit loop never waits on them. CPU only, and every milestone is sized to train in an evening on one desktop, because a project that needs a GPU to make progress is a project that stops.
It also ran on a wire. The trained policy — M4a's median seed, exported as a committed .npz and run through a numpy forward pass with no training stack on the client's import path — worked a 1,000-share parent order over 13 bins on a live Anvil book, another project in this portfolio. It is that protocol's third independent client after the browser UI and DepthCharge, and it required zero changes to Anvil. It reports 11.21 basis points of arrival slippage.
The number is not the result. It was predicted first. A committed counterparty ladder plus a deterministic policy plus deterministic matching makes every fill price and quantity computable in closed form before anything is sent — and three ladder shapes came back matching level for level: 11.21 bps on the reference book, 22.85 on a deliberately thin one, 33.63 on a wide-spread one, each to the digit and each reproduced across two revisions. A dozen lines of Python on one side, a C++ matching engine over a wire on the other. Arrival slippage measured against a book the client built itself was never going to say anything about execution quality; predicting it first makes it say something about client correctness, which is the one thing it can certify.
The thin book is where the closed loop stops being decorative. The simulator always filled exactly what was asked — its step function clips the trade to the inventory available — so a partial fill is a state the policy had never seen in training. Bin one asked for 421 shares against 375 of depth, swept all eight price levels, was filled short, had its resting remainder cancelled, and carried the shortfall into the next observation. Anvil has no market orders, so an aggressive order is a limit priced to cross, and a limit that doesn't cross rests silently: accepted is not filled.
The same client then worked the same order against the public deployment over TLS, on a shared and unauthenticated book. It filled 1,000 of 1,000, attributed every share, and reported the measurement void — 236 fills by other participants landed while the order was being worked. That is the milestone's own pre-stated condition firing for exactly the reason it was written: a third-party fill makes the run a successful demonstration and a void measurement, and both halves get reported. It is the only void run of the five, and it is void for the healthiest reason available — the venue was real and other people were trading on it. A clean number there would have meant nobody else was.
This is plumbing evidence and not execution-quality evidence, and the distinction is the point: the flow is a synthetic non-adversarial feeder, the sample is one order, and there is no baseline it could fairly be compared against. What it establishes is that the policy speaks a versioned venue protocol end to end — session, order entry, verdict channel, event stream, and a book that does not give it what it asked for.
// Key decisions
Why is the agent never allowed to beat Almgren–Chriss?
Under the exact assumptions the closed form makes, it is provably optimal — so an agent that scores better has not found something, it has found a bug in the harness, the metric or the environment. Phase 1's claim is convergence to within a pre-stated tolerance, and a hard test fails the run if any seed scores below the certified optimum by more than arithmetic noise. Advantage is only claimed in Phase 2, where the assumptions are deliberately broken — the closed form is solving the wrong problem in the first of those worlds and has no answer at all in the other two. The test survives that, and by the last milestone it stops being a threshold and becomes a proof: the graded cost splits into a part the signal can move and a part it cannot, the second is convex and contains no signal, so the certified optimum of the earlier world is a floor under it for any policy whatever. An agent below that floor is a defect with an argument behind it rather than a seed that got unlucky. The distinction between those two situations is the whole project.
Why a separate repo from FrontierView, rather than importing its model?
The boundary is versioned artefacts, not shared code: FrontierView's outputs are exported by a read-only script and vendored here with the source commit stamped, and nothing upstream changed for Temper's benefit. Independently re-deriving the closed forms costs about 55 lines of parallel code — and it is what surfaced the κ convention problem, which a shared import would have inherited silently. A reference engine that shares code with the thing it is checking is not a reference engine.
Why is the agent graded analytically instead of by Monte Carlo?
Through Phase 1 the observation carried no price at all, so a deterministic policy produced the same schedule under every price draw — and a fixed schedule's exact cost is a closed form. One rollout gives the answer with no sampling error at all. That is not merely faster: resolving the milestone's tolerance by sampling would need on the order of 10⁷ episodes per seed and would still report an interval where this reports a number. The shortcut is made sound rather than merely convenient by an assertion that rolls the policy out on two unrelated random streams and requires the schedules to match bit-for-bit, so it fails loudly the moment price reaches the observation. Stochastic liquidity half-retires that, and names which half was load-bearing. The observation now carries liquidity, so the schedule reacts and there is no single trajectory to hand to a closed form — but the price still never enters the decision, so conditioning on the liquidity path removes all of the price randomness analytically and the conditional expectation is a closed form again, averaged over 20 000 held-out paths with no price sampling anywhere. The rule was right and its reason was narrower than its wording: what made analytic grading legitimate was never that the schedule was open-loop, it was that the price never entered the decision. And then the alpha signal moved the line the rule is drawn on, for the first time since it was drawn: the observation now carries something that predicts the next bin's shock, which is price-bearing on purpose. What replaces the old ban is a guard in two clauses — pin the signal, vary the price, and the observations must come back bit-for-bit identical; and no coordinate may correlate with any shock the decision being made can no longer change the cost of. The realised price in any form is still refused, so is any realised cost, and so is a signal about a shock that is already committed however far ahead it lands. Grading follows the observation rather than trailing it: conditioning on the signal path makes the training reward the conditional grade itself, with zero conditional variance and no price sampling anywhere in the milestone, and a check refuses outright any grade whose conditioning set is not exactly what the policy saw.
Why write the success criteria down before training?
Because post-hoc thresholds are how reinforcement-learning projects mislead their own authors. Each milestone's brief fixes the tolerance, the seed count and the confidence level before any training code runs, and loosening one means amending the brief before the work, in the commit history. It has bitten usefully: the tolerance inherited from three earlier milestones turned out to be 1.8× the size of the effect the next milestone was measuring, which was visible only because the bar had been written down and could be compared against the oracle in advance.
Why does the oracle carry two decay rates instead of one?
Because the vendored library's κ is a continuum-limit expression and the discrete problem's stationarity condition gives a different one — about 2.2× apart at realistic order sizes, and both produce sinh-shaped schedules, so the difference is invisible on a chart. Keeping both means the goldens still match the vendored library exactly, while the agent is graded against the optimum of the objective it is actually trained on. A test pins the mapping between them and a guard fails if anyone collapses them back together.
Why Python, when the rest of this portfolio is C++?
The reference model's compute core is Python, the RL ecosystem is Python, and the C++ story is already told by Anvil and Crucible. The interesting engineering here is the grading discipline, not the language. A C++ inference leg — exporting the trained policy and measuring it under load — is on the backlog as the thing that would tie the two halves of the portfolio together.
Your live run reports a number and then says it's void. Which is it?
Both, and they answer different questions. The run is a successful demonstration: the client held a session, submitted orders, consumed the event stream, reconciled every fill to its own order ids, and finished the parent order. The measurement is void because 236 fills by other participants landed on that book while the order was being worked, and the condition for voiding was written into the milestone brief before the run happened, not after seeing the number. The reason to report it that way rather than quietly publishing the figure is that a clean number from a shared public book would have been the weaker result — it would have meant nobody else was trading, which is the opposite of the thing the run exists to demonstrate. The measured runs are the ones against a book the client built itself, where every fill was predicted in closed form beforehand and matched to the digit. Void is a result, and there is deliberately no reconciliation path in the code: the moment one exists, every number the client produces becomes an estimate.