A strategy is a sentence. We turn it into a function. Same input always gives the same output — testable, predictable, boring. Boring is the goal.
An IF … THEN … rule. Numbers, times, comparisons. No fuzzy words.
# your rule, in English
IF time is between 09:17 and 09:31
AND NIFTY straddle decayed ≥ 1%
THEN sell 1 lot of the ATM CE
at a LIMIT order.
Inputs go in, an action comes out. Pure. No surprise side effects. Easy to test.
def decide_entry(state): if "09:17" <= state.now <= "09:31" \ and state.straddle_drop >= 0.01: return SellAction( symbol=state.atm_ce, lots=1, order_type="LIMIT") return None
IF … THEN … rules now exists as a Python function in a file called strategy.py.pytest.None.pytest. By Sunday they should feel ordinary.# strategy.py — your first decision function from dataclasses import dataclass # The shape of the market state we'll pass in @dataclass class MarketState: now: str # "09:25" atm_ce_symbol: str # e.g. "NIFTY24MAY24800CE" open_straddle: float now_straddle: float def decide_entry(s: MarketState): # window check — between 9:17 and 9:31 if not ("09:17" <= s.now <= "09:31"): return None # decay check — straddle down ≥ 1% from open drop = (s.open_straddle - s.now_straddle) / s.open_straddle if drop < 0.01: return None # fire return { "action": "SELL", "symbol": s.atm_ce_symbol, "lots": 1, "order_type": "LIMIT", }
s = MarketState( now="09:25", atm_ce_symbol="NIFTY 24800 CE", open_straddle=300.0, now_straddle=296.4) action = decide_entry(s) assert action["action"] == "SELL"
s = MarketState( now="09:40", # too late atm_ce_symbol="NIFTY 24800 CE", open_straddle=300.0, now_straddle=280.0) action = decide_entry(s) assert action is None
IF … THEN … rules. Pick the most concrete one.test_strategy.py.pytest -v. Both should pass. If one doesn't, the rule and the test disagree — figure out which is wrong before changing anything.MarketState. Read what the function would have done.The rule said: wait for the price to break below ₹95, then sell. The code placed a LIMIT SELL at ₹95. The current price was already ₹103. The exchange saw a sell offer at ₹95 — much better than the bid — and matched it instantly at ₹103. No breakdown. Just an instant fill at the wrong price.
The exchange's logic: "sell at 95 or better." 103 ≥ 95, so it fills at 103 — better for the seller, terrible for the strategy. The breakdown never happened.
An SL (stop) order waits — it does nothing until LTP touches the trigger. Only then does the sell go live. This is the correct order type for "wait for breakdown."
| When you want to… | Order type | Behavior | Common mistake |
|---|---|---|---|
| Sell now, at any price | MARKET | Fills instantly at next available price. | Indian options: not allowed via API on many segments. |
| Sell now, but cap the price | LIMIT | Fills at limit or better. If better is available, you get the better price — instantly. | Using it for "wait for breakdown" — fills now, not later. |
| Sell only after price drops to X | SL-M / SL-L | Dormant until LTP hits the trigger. Then becomes a MARKET / LIMIT. | Setting trigger above current price — fires immediately. |
A LIMIT order is not a "wait until" order. The exchange will happily fill it now if the book is more generous than your limit. Waiting for a price to be crossed is what Stop orders (SL-M, SL-L) are for. The decision function must encode this choice.
So when you write your function this week and it returns an action, ask: does this action mean "fire now" or "fire when X happens"? Pick the right order type for that intent. Pick wrong and the strategy will misbehave on day one.
If you can hand someone your strategy.py and test_strategy.py, they can read the rule, run the tests, and trust the behavior. That's the real product of this week. Hooking it to a broker comes next.