Building a multi-strategy crypto quant system. Backtest-vs-live gaps, data & execution traps. NAV on vBase — numbers later. No calls, no picks, research only.
I'm building a multi-strategy quant system for crypto. This account documents the build: factor and portfolio methodology, backtest-vs-live consistency, the traps in data and execution. Build in public — the thinking is public, the recipe is not.
I like this way of looking at it. Though I think there's another skill somewhere in there: figuring out how much of the alpha you discover can actually survive live trading.
You can spend months building a new dataset or some clever infrastructure, find something that looks genuinely promising, and still watch a lot of that edge disappear once execution costs, slippage and real market conditions get involved.
Getting from a good research result to something you can reliably trade is almost a separate discipline. Maybe not as exciting as creating new alpha, but I'd argue it's just as difficult.
That bit about drawdowns looking easier once you can see the recovery is so true. A smooth equity curve built from thousands of trades can make the risk feel nicely spread out, but those trades aren't necessarily independent when the market starts falling fast.
A bunch of different mean-reversion signals can end up buying into the same selloff, so the losses arrive together rather than one by one. I think that's the uncomfortable part. You can have a perfectly decent win rate over the long run and still get hit pretty hard when all those supposedly separate trades start behaving like one big position.
Yeah, and there's another annoying part when you try to backtest this. If your stop and target both get hit within the same candle, you don't really know which one came first unless you've got more granular data.
With the kind of small moves you're talking about, that detail can make a pretty big difference. An 80% setup might look great on paper, but I'm not sure how much of that survives once you account for the actual path, spreads and fills. That's the part I'd be more interested in.
The 58 risk-off months caught my eye. That sounds like a decent amount of history, but those months are clustered into just 12 episodes, and probably even fewer genuinely independent stress events.
I'd put more weight on how the rule performs when you remove the major crises one at a time, especially 2008. If the drawdown improvement still holds up, that would tell me a lot more than another round of threshold sensitivity tests.
My concern is that a nearly 30-year backtest can look pretty robust when most of the protection may have come from getting a small number of crises right.
I Turned Off a Risk Control. Then the Strategy Changed.
I was reviewing the drawdown of a cross-sectional strategy when I found myself going back to a risk control I'd switched off earlier.
The odd part is that I still think the original decision was reasonable. What had changed was the strategy. I was trying to use an old test result to answer a question about a different version of the system, and the more I looked at it, the less comfortable I was with that.
It made me rethink something pretty basic: how long does a research conclusion stay valid?
Early on, I kept a relatively simple equal-weight portfolio. I also had a way to scale the entire portfolio based on the strategy's own volatility. The idea was straightforward enough. When the strategy became more volatile, it would carry less exposure; when things settled down, exposure could recover.
That extra layer didn't show a convincing benefit in the version I was testing. I didn't see a good reason to keep another moving part just because the idea sounded sensible on paper, so I turned it off.
Then the strategy changed.
Instead of simply spreading exposure across the portfolio, the selection process began using momentum to rank and choose instruments. That sounds like a change to the signal, and it was. But it could also change which risks ended up concentrated in the portfolio, even if the way I measured volatility stayed the same.
So the old conclusion — volatility scaling doesn't help here — was no longer something I could just carry forward. I had tested a different strategy configuration. That's a much narrower statement.
The reason I came back to it wasn't a sudden enthusiasm for adding more controls. It was the drawdown. I could keep working on the selection logic, but if the portfolio's exposure was poorly matched to the risk it was taking, the resulting equity curve could still be pretty hard to live with.
I tried looking at the problem from the outside first, using broader market conditions to adjust risk. The historical comparisons didn't make a convincing case for adopting that approach. So I went back to the strategy's own volatility layer, the one I'd already written and then disabled.
Its job wasn't to improve the momentum score or change which instruments got selected. It was to look at how volatile the unscaled strategy had been and adjust the portfolio's total exposure accordingly. More volatility meant less exposure. Lower volatility allowed the allocation to move back toward its normal level.
That distinction matters more than it might seem.
If a risk control reduces drawdowns or improves risk-adjusted results, it doesn't follow that the selection signal has become better at identifying winners. Part of the improvement may simply come from taking less risk in the periods when the strategy is swinging around the most. That's useful, but it's a different kind of improvement, and I'd rather know which one I'm getting.
One reference in this work was Barroso and Santa-Clara's 2015 paper, Momentum Has Its Moments. The paper's abstract makes an interesting argument: momentum risk changes over time, some of that variation is predictable, and managing the risk dynamically can help limit momentum crashes.
I want to be precise about how I used it. I didn't reproduce the authors' tables, copy their parameters into my system, or run a faithful replication of their original experiments. I used the underlying risk-management idea as something to test against a strategy I was already building.
And in the historical comparisons I had, bringing the strategy's own volatility control back improved the risk profile. I decided to keep it enabled, without adding leverage to amplify the result.
That doesn't make the evidence cleaner than it is, though.
The historical windows I looked at overlapped. They weren't two independent out-of-sample tests, however tempting it might be to describe them that way. The results also relied on proxy prices. Real execution conditions, the prices actually available to trade, and the full effect of transaction costs weren't established by those comparisons. I can't turn that into a claim about what would happen live just by writing a more confident conclusion.
What the work gave me was a specific decision: for the strategy version I had actually tested, there was a reasonable case for keeping the volatility control on. It did not prove that I'd replicated the paper, and it certainly didn't establish that the same approach would work for every momentum strategy or every market.
Looking back, switching the control off and later switching it back on aren't contradictory decisions. They came from different versions of the strategy, facing different risk problems.
The mistake would have been treating the first decision as permanent. A result has conditions attached to it, even when those conditions aren't written in big letters in the research notes. Change the selection process, portfolio structure, or source of risk, and a result that used to make sense may deserve another test.
The paper gave me a useful mechanism to examine. My own data had to decide whether I kept it.
There's still another problem waiting on the other side of that decision. Even when the risk-control formula looks right, how do I know the backtest is actually applying it the way I think it is? That's what I want to unpack next.
I think the move from 40% bonds to no bonds at all needs a bit more unpacking. The real-return argument makes sense, but by the time bonds get to zero, you've also expanded the set of assets the optimizer can choose from. That's a pretty big change on its own.
I'd like to see the same universe optimized using nominal versus real returns first, then add the extra assets while keeping everything else constant. Otherwise it's hard to tell how much of the result comes from adjusting for inflation and how much comes from bonds simply losing out to the new alternatives.
this makes me think about how a volatility target would behave in a retirement account. if it only sees a matched TIPS holding swinging around in price, it might tell you to cut the position even while it's doing its job against future spending needs. you could end up with a quieter account balance and less of the hedge you actually wanted.
showing the account value and the income it can fund side by side would make that trade-off easier to see. otherwise the red number on the statement gets all the attention.
The Alarm Failed. The Dashboard Still Looked Fine.
I'm building a crypto quant system that needs to keep running without someone watching every job. That puts a lot of weight on the monitoring. If a data check fails or a service runs into trouble, I need a reliable way to find out. What took me longer to appreciate was that the monitor itself can fail, and its failure can look exactly like a quiet day.
I'd already found two versions of this problem. In one, a data read failed, but the warning never made it into the main log where it could be seen. In another, an exception occurred, yet the dashboard treated it as expected noise and folded it away. Neither case meant the underlying job was healthy. They meant the path from a fault to a visible warning had broken somewhere along the way.
That's awkward because a monitoring dashboard is easy to trust. You open it, see no red marks, and move on. But the screen can only show what the rest of the reporting chain manages to deliver. A failure has to be noticed, represented correctly, passed along, and ultimately put somewhere a person can actually see it. I had evidence of gaps in more than one of those steps.
A later investigation found a particularly small one. The dashboard's status check recognized one spelling of a failure state, while the underlying output used another. The program could report a failure and the panel could still miss it. This wasn't a sophisticated race condition or an exotic edge case. It was an ordinary mismatch between what one component wrote and what another expected to read. Small differences like that are annoying to investigate because everything looks locally reasonable. The reporting side has produced a status. The dashboard has a rule for identifying failures. Until you compare the two directly, it's easy to believe they fit together. In this case they didn't.
There was also a warning from a backup path that had nowhere useful to go. The warning existed, but there was no effective recipient. That's a different kind of gap. A correctly classified message still doesn't help if it never reaches anyone. It's possible to repair the dashboard's interpretation of an alert and leave the delivery problem untouched.
Both findings changed what I wanted to test. Checking whether the code can generate a warning isn't enough. I need to see whether an actual fault can travel through the reporting path and come out as something visible. Otherwise I'm testing the first link and assuming the rest of the chain works.
The later fixes addressed those concrete gaps. The dashboard's failure classification was corrected, and the warning-delivery path was repaired. A controlled error in a separate record-copying path was then used to produce a real warning, rather than just drawing a red box on the dashboard. That warning output was connected to the panel's decision about what counted as a failure. Tests also deliberately broke the sending and receiving sides of the reporting path. In those local tests, the failures were detected.
That was useful evidence. It showed the repaired path could respond to deliberately introduced faults, including faults on either side of alert delivery. The fixes were deployed, and these local checks passed. I can say that much without stretching the result.
What I can't say is that the entire monitoring system now watches itself.
The two older failure paths still don't have documented, one-by-one post-fix acceptance tests covering the full chain. And I don't have evidence of an independent health check that verifies the alert checker itself is alive. Nor do I have a complete drill in which the checker fails and another mechanism reliably reports that failure. Those are different claims from “the alert-delivery fix passed its tests.” The records support the latter, not the former.
There's an important distinction here with the other verification jobs in the system. I also run independent checks on recorded results. Those can tell me whether a business record agrees with another record, or whether a particular verification job completed. They don't automatically tell me that every alarm channel is functioning. A successful verification result isn't proof that the machinery responsible for reporting failed verifications is healthy.
So my current boundary is fairly narrow. Some missing alerts had identifiable causes; their fixes are deployed; selected failure-injection tests passed. But an unattended system needs more than proof that individual warning paths work when tested. It needs evidence about what happens when the thing doing the checking goes quiet, particularly when nobody happens to be looking at the screen.
I haven't closed that loop yet. Until I can test the monitor failing, and see a separate path reliably tell me about it, I won't describe this as a fully self-monitoring system. The green dashboard is useful. I just want better evidence for what that green actually means.
The interesting question isn't how many stocks agents can cover, but how much independent evidence they produce.
Scaling research to 3,000 names is impressive. But if agents share the same data biases and assumptions, you're scaling correlated errors too.
I'd be more interested in source-level auditability, independent validation, and whether those scores actually hold up out of sample.
Coverage scales easily. Research quality is harder.
The interesting part isn't volatility forecasting, but managing the transition.
A vol estimate can be accurate on average and still fail exactly when risk matters most.
I've been looking at a similar problem in regime-adaptive portfolios: reducing exposure during transitions without overreacting to every short-term volatility spike.
The trade-off is response speed versus false alarms. React too slowly and drawdowns expand. React too quickly and you sacrifice returns through constant rebalancing.
the 5% bond yield is the part that can get read as already in the bank. over a year, a bond fund can still lose money if yields rise enough, especially with longer duration. then the stock side has a different hurdle to clear.
i'd read those bars as scenarios where the bond sleeve actually earns the assumed return. without keeping that assumption in view, it's easy to focus on the 8.3% needed from stocks and forget that the bond contribution can move around too.
paying suppliers later can make operating cash flow look better for a while, which is an awkward wrinkle here. the business might be leaning harder on its suppliers just as the cash flow number starts looking healthier.
i'd put reported OCF next to a version with the payables contribution taken out. that alone wouldn't tell you whether the company was in trouble, but it would show how much of the improvement came from letting bills pile up. worth separating that out before treating stronger OCF as a sign of recovery.
The Scheduler Kept Firing. I Still Lost a Day of Records.
I'm running a crypto quant system with a fair amount of scheduled work: strategy runs, status checks, data collection, and separate verification jobs. Most days I barely think about the scheduler. If the jobs are scheduled and nothing complains, it's tempting to assume everything happened. A few incidents made that assumption pretty hard to defend.
The first one was almost absurd. The scheduled calls kept coming for an entire day, but the actual business logic never ran. A diagnostic log statement added during initialization referenced an object that didn't exist in that scope. The normal path threw an error, and the exception handler tried to use the same missing object, so it failed too. Every execution mode went through that initialization. The scheduler launched the job as expected, but the process died before it could do any useful work. By the end of the day, the daily stats had a hole in them.
The tests hadn't caught it. The strategy tests checked calculations and behavior; the backtest entered through a different path. Neither exercised the real production startup sequence, and the static review missed the scope problem as well. I added a startup smoke test that walks through the real initialization, data loading, strategy setup, and state recovery, then exits before it can place a trade. That check now sits in the deployment gate. The incident record doesn't establish which alert first caught the failure, so I can't claim our monitoring spotted it immediately, let alone fixed it automatically.
The next scheduling problem was messier. It actually led to trades.
I'd been doing some manual cleanup and reconciliation when the system later decided it needed to catch up on a missed job. The catch-up rule used the ledger's last-update time as a proxy for whether the monitoring task had completed. At first glance that sounds reasonable. If the date is old, perhaps the job didn't run. But ordinary reconciliation can update a ledger without completing a monitor, and a monitor can finish without updating the ledger. Those timestamps describe different events.
On a subsequent restart, the old rule triggered a catch-up run. That run executed a full strategy cycle, which opened positions allowed by the strategy's conditions at the time. When the regular scheduled run happened later, the conditions had changed and the system adjusted those positions again. Looking at the sequence after a manual cleanup, it was easy to suspect that state recovery had quietly brought back positions we'd removed.
That was the first working theory. Two backups ruled it out: neither contained the positions we thought had been restored. Changes in the execution logs pointed instead to a complete strategy run in between. The positions hadn't been resurrected from an old state file; the catch-up mechanism had run the strategy again. Those trades followed the strategy rules, so calling them erroneous orders would be misleading. The scheduling decision was the issue: I'd used a timestamp that couldn't actually tell us whether the monitoring work was finished.
I changed the catch-up check to use its own completion record, written only after the monitoring job succeeds. It has to survive a restart as well, which means saving it and recovering it correctly, not just keeping a value in memory.
The initial tests were green. The field existed, the helper worked, and the logic appeared to be in the right place. Then an independent retest removed the restore call and, in another case, emptied the saved value. The tests stayed green. That was uncomfortable: the scheduler could silently fall back to the old ledger-timestamp check, and the tests I'd thought protected the fix wouldn't notice. The fix needed a real persistence round-trip test: write the completion record, save it, reload it, and check what the scheduler actually receives. Testing each function on its own hadn't tested the connection between them.
There was a third gap that happened even earlier in the chain. I'd added a separate verification job. Its files were in the repository, and the dashboard already had a place for it. Looking at either one, you'd think the job had been deployed. But the service hadn't actually been installed or enabled on the machine. The dashboard knew its name; the system responsible for starting it didn't.
After installation, a manual run and then the first automatic run both succeeded. Later records also showed successful completions. The original incident notes needed a correction, though. They claimed an earlier scheduled run had been missed. The job was actually submitted after that scheduled time, so there was no legitimate run to miss. The real gap was between code, dashboard configuration, and an installed service—not a proven missed execution at that earlier time.
So I'm much less comfortable with a single green label saying “scheduler healthy.” A trigger firing doesn't mean startup succeeded. A successful startup doesn't prove the business work finished. A field existing doesn't prove its value survives a restart. And a dashboard entry doesn't mean a service exists on the machine.
There are still limits to the fixes. The catch-up decision runs at process startup; if a monitoring job fails while the process stays up, that failure doesn't automatically trigger a catch-up. We also haven't run a full production-style replay of the original failures after the repairs. Nor do these records show a duplicated trigger, the wrong job being launched, or a job run with the wrong parameters. I'm not going to turn those possibilities into stories that happened.
For now, the changes are specific: a real startup check, a completion record that survives restart, a test that verifies that whole round trip, and proof that the independent job really runs on the machine. I still want to know what happens when the thing responsible for reporting these failures goes quiet. That turned out to be another problem entirely.
forced selling is what comes to mind for me in crypto. someone can still be bullish and have their long liquidated anyway. once that happens, their opinion isn't deciding the trade anymore.
then you have something concrete to look for in the data: whether the move continues while positions are being closed, or starts to unwind after that selling eases off. i wouldn't assume either outcome, but having an actual reason why someone has to trade gives the research a place to start
the forward-vol denominator can do something pretty sneaky here. if a feature catches a big move, that same move also pushes up the denominator, so the feature can end up looking less predictive right when it actually worked.
i don't think it's necessarily look-ahead leakage if you're only using it to evaluate labels, but it does change what you're measuring. i'd plot the raw forward returns alongside a version scaled by vol known at t, using a lookback closer to the holding horizon. if the bins still line up in both, that's a lot more convincing.
the nasty thing about news slippage is how uneven it can be. you might have hundreds of perfectly normal fills, then one big release comes along and gives you a price that's nowhere near what the backtest assumed.
that's also why average slippage numbers can be pretty misleading. the worst few fills are often what really matter, and a candle-based backtest can make those moments look a lot cleaner than they were.
that beta-adjusted chart on the right is the interesting one. there's still a huge jump at the end, but SPMO can get pretty concentrated in a few sectors. tech alone was over half the index recently.
some of that outperformance might be coming from those sector bets rather than stock-level momentum itself. a sector-neutral version of the same backtest could look quite different. especially during a run where a handful of industries are doing most of the heavy lifting.
The Eight-Second Check I Didn't Want to Keep Doing by Hand
I'm building a crypto quant system, and the research, backtests and execution are only part of the work. One less interesting job was checking that my records had been anchored on-chain. I keep them so I can verify later that the history hasn't been altered, but I still need to compare the on-chain result with the records saved locally.
For a while I did that check myself. The script was already there. I'd run a command, wait for the result, look for anything odd and move on. It was easy enough that I barely thought about it. Then I realized the check only happened if I remembered to run it. A failing script might leave an error. A script I never run won't. The system just keeps going.
It was a tiny job, but it was still one more thing I had to remember.
So I moved it into a separate scheduled task. Initially I also wanted to cut the scope of the check. If I'm mostly concerned about the latest record, why go through the older history again? Before changing that, though, I measured where the time was actually going. The whole check took about 8.6 seconds. Of that, querying the on-chain history took roughly 8.45 seconds. Scanning and checking the local history took only about 0.15 seconds. I'd been looking at the wrong part. The local check I was about to remove was barely costing anything, so I kept the historical integrity check and the existing cross-checks. I only narrowed the on-chain query to the timestamp of the latest record. That brought the run down to about 4.3 seconds.
Cutting the runtime roughly in half was nice, but those few seconds weren't really the point. I haven't measured how much human time it saves, either. The useful change is that I no longer have to run the command myself. The first automated run completed successfully, and later logs show successful runs too. At least for this one routine check, there's actual evidence that the task has moved out of my hands.
That got me looking at another maintenance annoyance: restarting services after code changes. Some of my jobs don't need a scheduler restart at all. The scheduler launches a fresh child process for the work, and that process loads the relevant code when it starts. Change that code, and the next run picks up the change. That saves unnecessary restarts and the risks that come with them.
But that doesn't work for every change.
I've deployed a new rule without restarting the service that needed it. The old behavior kept working, but the new rule never loaded. The difference was when the code got loaded: at the start of a short-lived job, or at the start of a service that stays alive. Now I have to check which path a change belongs to. Seeing the old functionality run tells me very little about whether the new code is live.
I found another gap in the proof checks. Automating one step hadn't automated the whole process. My offline verifier could recalculate and inspect the historical records, but comparing the latest signed record with the corresponding entry on the server-side ledger wasn't a required step. A person had to do that comparison when needed. A successful exit code didn't mean all the evidence had been compared. I updated the verifier to show the digest needed to check the latest signature, and made comparing it with the matching ledger entry an explicit check. I ran that comparison end to end, and the records matched.
I know the fields are now available, and I know that comparison passed. I can't claim future exceptions will all be detected and handled without someone stepping in.
And there are cases where I don't want the software making the decision anyway. Say an on-chain write returns an uncertain result. It may have failed, or it may have succeeded and the confirmation simply didn't make it back. Retrying blindly could create a second problem. For now, I want an alert, a check of what actually happened, and a human decision on what to do next. I'm keeping that boundary.
I also separated the collection of cash-flow and position records from the trading jobs. Those records matter when I need to reconstruct what the system did, and I don't want their collection to depend entirely on a trading task completing successfully. They now have their own collection path. Transfer records are deduplicated by a unique identifier, and the collection and deduplication logic have passed their tests. That removes one dependency, but doesn't reconcile the books for me. A cash-flow record tells me a transfer happened, and a position snapshot tells me what was held. If there's a loss, neither one can automatically tell me whether it came from trading, a transfer or another account change. That still takes judgment.
Looking back, none of this was a new algorithm: just scheduling a manual check, knowing when code really needs a restart, making a signed record easier to verify, and collecting account records independently of trading. What connects them is how often a running system still relies on somebody remembering to do something, or knowing when to step in.
Some routine work is now running automatically, and I have tests and logs for parts of the rest. That's not enough for me to call the whole system unattended. Uncertain writes, recovery from failures and attribution of losses still have clear human boundaries. I'd rather keep those visible than pretend they've been automated away.
There's another problem with scheduling, though. When a job doesn't run, should the system catch up? I had a catch-up run trigger when it shouldn't have. That one deserves its own write-up.
the portfolio fit check can quietly turn into another optimization step. once you're generating lots of candidates and keeping the ones that look best alongside your existing systems, you're also selecting for some lucky correlations in the historical data.
each strategy might pass its own robustness checks, but the combined portfolio can still look better in the backtest than it does live. kind of a sneaky problem, because adding something for diversification feels like risk management, not another round of optimization.
https://t.co/oCkuQeezXP
Added a signal. NAV didn't budge.
Each new signal gets one question: did the book move? One three-arm test gave me three identical NAVs. Basis/momentum corr -0.1, not a dupe on paper. Turns out its basket never hit min size, so it never traded. Nothing got in, nothing changed.
I agree, looking at the whole portfolio is what matters here. the time spent underwater is worth watching too. sometimes the worst drawdown barely changes, but the recovery gets a lot shorter, which makes a big difference when you're actually running the systems. hope it holds up live.