Week 08 · Build Your Own Automated Options Trading System
Safety nets — hard limits
in code.
A strategy without coded limits is a confession that you don't really believe your own rule. This week we encode the limit, the SL, and the position size — and we make the bot ask the broker before doing anything risky.
This week, in one glance
Same five beats. New topic.
1
Idea
A strategy without coded limits is a confession you do not believe your own rule.
2
Outcome
Your function also returns an SL price and a position size based on a risk you choose.
3
Vocabulary
atomicity · race condition · watchdog · fail-safe · idempotency
4
Hands-on
Set a 50% SL. Print entry & SL side by side for three trades. Decide if you can sleep.
5
War story
SL filled milliseconds before our cancel reached the exchange. Emergency exit fired anyway. We bought twice what we sold.
1 · The idea
If the limit is in your head, it isn't a limit.
Limit lives in your head
"I'll exit if it goes against me by 50%."
# strategy.py
enter_position()
# (SL? we'll see)
Five minutes later, the price is 70% against. You convince yourself it'll come back. It doesn't. Real money goes.
Limit lives in the code
The function returns the SL price at the same moment it returns the entry. No second decision.
# strategy.py
action = decide_entry(state)
# returns:
# {
# "entry_price": 68,
# "sl_price": 102, # +50%
# "qty": 65,
# }
The bot places the SL in the same breath as the entry. There is no "later" decision to flinch on.
Discipline you have to muster is willpower. Discipline you can't escape is engineering.
Position size by risk, not by feeling
Three numbers. One formula.
The decision function now returns three things, not one: an entry, an SL, and a size — all derived
from a single risk number you chose before the market opened. Choose the risk on a calm Sunday.
Live with it on a noisy Tuesday.
Position size is the loudest knob in trading. Set it from risk, not from "let's go big today."
2 · By the end of this week
Three numbers come out of the function.
Your decide_entry() now returns entry, SL, and quantity — all three computed from a single risk_per_trade setting.
You've printed the entry and the SL side-by-side for at least three pretend trades. The SL distance no longer surprises you.
You've added a cancel-verify wrapper around any cancel call — never assume a cancel happened until the broker confirms.
You can articulate, in one sentence, what your strategy will lose on its worst day. If you can't, you don't have a limit — you have a hope.
"Can I sleep tonight if the worst case happens tomorrow?" If yes → the code matches your risk. If no → the code is louder than your courage.
3 · New vocabulary
Five words. Use them this week.
atomicity
"All or nothing." A multi-step action either fully completes or fully unwinds — never half-done.
place CE + place PE → both or neither
race condition
Two things happening at almost the same time, where order decides outcome. Network ↔ exchange is full of these.
cancel ↔ fill arriving
watchdog
A separate, simpler process that watches your strategy. If it falls silent, the watchdog raises the alarm.
cron · health probe · heartbeat
fail-safe
The default behavior when something is unknown — choose the option that limits damage.
unknown state → exit, not retry
idempotency
Doing the same thing twice has the same effect as doing it once. Critical for retries.
PLACE-ORDER-ABC-001
These five are the safety vocabulary. The next two slides show one of them — race condition — in real-money form.
5 · A real-money war story · BUG-100
The SL filled milliseconds before our cancel reached the exchange.
The strategy saw price spike up, fired a "jump exit" — cancelled the SL, then placed an emergency buy. But the SL had already filled at the exchange in the time the cancel was in flight. The emergency buy went through anyway. We bought 120 units against 60 sold. Real money.
The race, in milliseconds
T+0ms
Engine
Price tick arrives → "SL jump" — decide to cancel SL and exit at market
T+5ms
Engine → broker
Sends CANCEL on the SL order (request in flight)
T+8ms
Exchange
SL order fills at the exchange (cancel hasn't arrived yet) — position is now FLAT
T+15ms
Engine → broker
Sends emergency BUY for 60 units (assumes still short)
T+20ms
Broker
Cancel rejected ("already filled") — but engine doesn't read this in time
T+50ms
Exchange
Emergency BUY fills → now long 60 units with no plan to be long
The fix — cancel-verify, then act
Ask the broker if the cancel actually happened.
order_lifecycle.py — cancel-verify pattern
"never assume the cancel worked"
# BEFORE (BUG-100) — assumes cancel succeeded
broker.cancel(sl_order_id)
broker.place_market_exit(qty) # ❌ might be duplicate
# AFTER (fix) — check, then decide
def cancel_and_verify(sl_order_id):
broker.cancel(sl_order_id)
# poll the broker for the SL order's final state
status = broker.get_order(sl_order_id)
if status.state == "FILLED":
# the SL fired before our cancel landed
# position is already flat — DO NOT exit again
return "already_flat"
if status.state == "CANCELLED":
# cancel succeeded — safe to place emergency exit
return "ok_to_exit"
raise RaceCondition("unknown state, halt and alert")
The race isn't avoidable. The cure is to ask, don't assume. Every cancel must be verified before the next action.
4 · The hands-on bit — your task
SL in the function. Size from risk.
What to do this week (90 min, paired with Claude)
- Pick a risk number for your strategy. Start at 0.5% of capital per trade. Write it as a single constant in
config.py.
- Extend your Week-6 function. It now returns three fields:
entry_price, sl_price (entry × 1.5 for a 50% SL on a short), and quantity.
- Compute the quantity from
capital × risk% divided by |entry − sl|. Round down to a whole number of lots.
- Print, don't trade. Feed the function three pretend market states. Print the resulting entry/SL/qty for each. Sanity-check by hand.
- Add a
cancel_and_verify() helper. It calls cancel, polls the broker, and returns "already_flat" or "ok_to_exit". Don't use it yet — just have it ready.
- Ask yourself the sleep test: "if all three of these fire and all three hit SL, what do I lose? Can I sleep on that?"
The right number is the one you can sleep on. The wrong number is the one your spreadsheet says is "optimal."
The lesson, pinned
Limits in the head are decoration. Limits in code are structure.
120
units bought against 60 sold — BUG-100
~50ms
race window between cancel and fill
100%
of cancels today go through cancel_and_verify
Three habits this week, in order of cost-to-skip:
(1) compute the SL price inside the decision function, never after the fact;
(2) compute the quantity from a chosen risk, never from a feeling;
(3) never assume a network message succeeded — verify with the broker before the next action.
Engineered discipline keeps working on the day your willpower fails. That's the entire reason to write the code.
Week 8 — takeaway
A safety net is three numbers and one verified cancel.
Entry, SL, quantity — emitted together from one function, derived from one risk choice. Every cancel verified at the broker before the next move. That is what "safe" looks like in code. Anything less is decoration.
End of Week 8
Next: Going live with one lot.
Week 9 — One lot · one tranche · one day →
source: cowork/Course_Outline_12_Weeks · Week 8 · BUG-100