
A few months ago I built a system where an AI can write trading strategies — actual Python files — and then run them against real market data.
The first thing it wrote was a simple moving-average crossover. Clean, reasonable, made money in backtest. Cool.
The second thing it wrote tried to import os.
This post is about what happened next, and the three gates I built to make sure the answer is always "nice try, but no."
Why let an AI write code in the first place?
In two earlier posts I talked about keeping an AI away from the order book — making sure it can't place trades directly. Those were about controlling what the AI wants to do.
This is different. This is about letting the AI write code, and then actually running that code. Not picking from a menu. Not tuning a few parameters. Writing a complete trading strategy from scratch, 80 to 200 lines.
Why bother? Because the interesting strategies aren't in the built-in library. A human quant might spend a week combining a Bollinger Band width filter with a volume-weighted entry and a time-decay exit. An AI can generate that in seconds. The creative possibilities are way beyond what you'd bother to hard-code.
But giving an AI the ability to write and run code introduces a new problem. The code might be dangerous — import os, read files, leak environment variables. Or it might be safe but worthless — an overfit noise machine that backtests beautifully on one slice of data and falls apart everywhere else.
Two independent problems. Two independent solutions. Sandbox catches the dangerous stuff. A fitness score catches the garbage.
What I was actually afraid of
Before writing any code, I sat down and listed every way an AI-written trading strategy could hurt me. Four categories:
Escape. Python has a famous trick: ().__class__.__bases__[0].__subclasses__() reaches into Python's internals and gives you access to basically any class — file handles, network sockets, subprocesses. No import needed. Pure runtime trickery.
Side-effect imports. Even a harmless-looking import runs that module's initialization code. If the AI imports the wrong thing, damage happens before you can blink. So interception has to happen before the import, not after.
Runtime hangs. A perfectly legal, perfectly clean function can still be an infinite loop or a memory bomb. The code looks fine to a static checker. It only misbehaves when you actually run it.
Garbage that passes. The code is completely safe. No dangerous imports, no weird tricks. It's also a hopelessly overfit mess — 300 trades on 500 bars, great numbers in-sample, zero predictive power out-of-sample. This isn't a safety problem. But it fills your strategy library with noise.
The important thing I realized: these four problems happen at four different moments. After the AI writes the code. When the code imports things. When the code actually runs. After the backtest finishes. No single check catches them all. You need defenses spread across the whole timeline.
Gate one: check the code before it runs
The first gate is the simplest one, and it runs before a single line of the AI's code executes.
I wrote a scanner that reads the AI's Python file and checks it at the syntax level — no execution, no imports, just looking at the structure. Think of it like a security guard checking bags at the door.
It has three lists:
Only these imports are allowed. math, statistics, collections, dataclasses, typing, enum, json. Seven modules, all pure computation, no filesystem, no network. Everything else the strategy needs — market data, order types, execution events — gets provided later by gate two. The AI doesn't need to import any of it.
These names are forbidden. eval, exec, compile, open, input, getattr, setattr, globals, locals — seventeen in total. They're the dynamic execution, reflection, and file I/O functions that no trading strategy should ever need.
No double-underscore attribute access. That __class__.__bases__[0].__subclasses__() escape trick? Blocked. Any attribute starting and ending with __ is denied, with five harmless exceptions like __init__. One rule kills the entire escape path.
Here's the part that actually matters in practice: when the scanner rejects something, it doesn't just say "no." It returns exactly which line, what the violation was, and a human-readable reason. The AI gets told what it did wrong and where, so it can fix it and try again. It's a linter, not a brick wall.
Gate two: give it only what it needs
Code that passes the scanner moves to the second gate. Now we actually have to load it — compile the Python and make it runnable. But we do it in a carefully stripped-down environment.
Every time we load a new AI-written strategy, we build a fresh sandbox from scratch. The sandbox contains:
- A cut-down set of Python's built-in functions.
abs,len,range,sum,sorted— about 55 names total, down from Python's usual 70-plus. Noopen, noeval, no__import__. The scanner already blocked these, but this is the second net in case it missed something. - All the trading system's symbols injected directly: what a bar of market data looks like, how to place an order, what side (buy or sell), what the clock says. The AI doesn't import these — they're already there in the sandbox when its code wakes up.
- One weird one:
__build_class__. This is a hidden Python function that makesclass Whatever:syntax work. If we don't include it, the code crashes with a confusing error. Is it dangerous? No — its interface is implicit and an AI can't realistically call it directly. Figuring out which "scary-looking" things are actually safe is most of the fun in sandbox design.
This is defense in depth. The scanner is the first net. The stripped-down sandbox is the second. If something slips past the scanner, it still can't reach anything dangerous at runtime.
Gate three: make sure it actually looks like a strategy
Code that passes gates one and two has been scanned and loaded safely. But is it actually a trading strategy? Or did the AI write a class that has nothing to do with what we need?
Three checks:
Is there exactly one strategy class? Zero classes that inherit from Strategy → rejected. Multiple strategy classes in one file → also rejected. One file, one strategy. Keeps things simple.
Does it actually respond to market data? A strategy needs a method called on_bar — the function that gets called every time a new bar of price data arrives. If the AI forgot to write it, the strategy is a paperweight that will never make a decision. Rejected.
Can the engine actually create it? When the backtest system starts a strategy, it passes in a few things: a name, a clock, a message bus, the instrument being traded, the timeframe. The AI's code needs to accept those. If its __init__ has the wrong signature, the engine can't create it. Rejected — but with a message saying exactly which parameters are missing.
The order of these three gates matters: scan the code (static) → load it in a sandbox → confirm it's a real strategy (structural). Each failure sends a clear reason back to the AI so it can rewrite and retry.
One more thing: run it in a cage
The three gates catch a lot. They don't catch a pure-compute infinite loop. The code is clean, uses only allowed symbols, looks like a valid strategy — and then runs forever, hanging the entire service.
So when we actually backtest the strategy, it runs in a separate process with CPU, memory, and time limits. If it blows up, it blows up the subprocess, not the main service. The rest of the system keeps running.
Also, every AI-written strategy gets tested side-by-side against "just buy and hold" — same market data, same starting cash, same fees. If the AI's fancy strategy can't beat the dumbest possible benchmark, it doesn't deserve a second look.
Honest part. There's one path where this subprocess isolation isn't in place yet: the live runner — the thing that trades on real-time bars while nobody's watching. Currently it runs strategy code inline. It relies on two human approval steps (someone promoted the strategy, someone started the run) to keep things trusted. Subprocess isolation for the live runner is on the list, not done yet. If you're deploying this yourself, you should know.
The other problem: safe code can still be garbage
A strategy can pass all three gates — perfectly safe, zero dangerous symbols — and still be worthless. 300 trades on 500 bars, a great-looking return number for the test period, falls apart the moment market conditions shift.
This is overfitting, and it's not a safety problem. It's a quality problem. The solution is a scoring function that doesn't let any single number fool you.
The score is a combination of four things:
- Sharpe ratio — return relative to volatility. The standard metric.
- Calmar ratio — return relative to the worst drawdown. This catches strategies that look good on average but occasionally fall off a cliff.
- Turnover penalty — subtract points for excessive trading. This stops the AI from gaming the score by churning through tiny trades.
- Drawdown veto — if the strategy ever lost more than 30% from its peak, automatic disqualification. One strike and you're out.
Why not just use Sharpe ratio alone? Because the AI figures that out. It discovers that high-frequency churn pumps the Sharpe number on paper, even though the strategy would get eaten alive by real trading costs. The turnover penalty says no. It also discovers strategies with great average returns that had one catastrophic month. The drawdown veto says no.
The coefficients — how much each factor weighs — are starting points, not gospel. They'll get tuned as we collect more data from real evolution runs. But the principle is more important than the exact numbers: no single metric gets to decide.
What the whole thing looks like
AI writes source code → Gate 1: scanner checks for dangerous patterns. Caught something? Here's what and where. Fix it and try again. → Gate 2: load the code in a stripped-down sandbox. → Gate 3: confirm it's actually a trading strategy. → Run it in an isolated subprocess, parallel with buy-and-hold. → Score it across multiple dimensions. → If it beats the baseline, it goes in the candidate pool. → Human reviews → Human approves → It trades.
The AI gets to be creative — it writes whatever strategy it can think of. But every line passes through three gates that don't care what it was trying to do. They care about structure. And at the end, a scoring function decides whether the result was worth running at all.
Not a single step in this chain says "trust the AI."
What I'd do differently
- The scanner is a lock, not a theorem. It blocks the things I know about. New escape tricks will show up, and the rules will need updates. This is not a mathematical proof of safety. It's a practical defense that gets better over time.
- The live runner needs the same subprocess isolation as backtesting. Right now it relies on human gates. That's the top hardening priority.
- The scoring weights are judgment calls. 30% drawdown veto, 0.3 Calmar multiplier, 0.10 turnover penalty — these numbers were reasonable starting points, not optimized values. Real evolution data will tell us if they're right.
- A single score loses information. Compressing "safe, profitable, stable, low-turnover" into one number is always lossy. A future version will track these dimensions separately instead of boiling them down.
I'd rather name the gaps now than pretend the whole chain is bulletproof. If you're building something similar, I recommend the same: ship what's load-bearing, name what isn't, and don't bluff.
The AI writes the code. Three gates decide if it ever runs. One scoring function decides if it deserves to.
No prompt engineering. No "please don't import os." Just structural checks that don't care about the AI's intentions — only about what's actually in the file.
If this resonated:
- 📬 Subscribe to Inalpha on Substack — one post a month on what I'm building
- ⭐ github.com/mirror29/inalpha — the three gates, scoring system, and full pipeline in
services/paper
Top comments (0)