Status: built 2026-09-12. Covers openfly/straddle, openfly/execution and
openfly/worker, their tests under tests/straddle, tests/execution and
tests/worker, and the names these packages expect from the neural, sensory,
readout, market and experiments packages.
Nothing in this layer is neural output. The engine, guard, ledger and brokers are ordinary risk engineering (docs/PLAN.md, section 1, rule 3).
GuardCheck(name, ok, detail),GuardResult(allowed, checks)withfailed,summary()("All 18 checks passed." or "The guard vetoed: ...") andto_dict()in the api-spec shape.GuardContext(dataclass): now, window, is_trading_day, quote, quote_age_s, prediction, vix, days_to_expiry, observation_index, index_now, day_pnl, entries_today, last_exit_at, in_position, lots, lots_detail, margin_available, margin_per_lot, stop_file_present, halted, halt_reason, pending_intent. The engine fills in what it knows (time, window, quote, prediction, VIX, P&L, counters, lots); the caller supplies the rest.Guard(settings).check_entry(ctx)runs, in order: trading_day, trade_window, last_entry, expiry_min_dte, vix_ceiling, spread, quote_age, index_move, daily_loss_limit, entries_per_day, reentry_cooldown, position_flat, lots_bounds, margin_available, stop_file, halted, pending_intent, prediction_present.check_exit(ctx)runs trading_day, halted, pending_intent, quote_present. Each check is also a public method.- Details are plain language, for example "10:20 is inside the trade window 09:20 to 14:30", "INDIAVIX 21.3 is above the ceiling 20", "last exit at 10:37, cooldown of 5 minutes runs until 10:42". Checks that cannot run for lack of data (no quote age in replay, no margin figure, no depth) pass and say "check skipped" in the detail. The spread check measures each leg's bid-ask width as a percentage of the combined premium.
State: FLAT, ENTERING, IN_POSITION, EXITING, HALTED.Action: ENTER, EXIT, STOP, STOP_LEG, TARGET, LOCK, SQUARE_OFF, REENTRY, HOLD, VETO, NONE.StraddleEngine(settings, cost_model, guard=None):start_day(window): resets the per-day counters on a new date.on_observation(observation, prediction, quote, window, guard_ctx=None) -> EngineStepon_tick(quote, now) -> EngineStep(action NONE when nothing happens).on_execution(intent_id, fills, status=None, detail="", stop_orders=None) -> EngineStep: the caller reports the broker outcome; the current step is updated in place (fills, snapshot, narrative).on_leg_stop(symbol, fills, now) -> EngineStep: a broker-side leg stop executed (action STOP_LEG).on_stops_placed(records): order ids and statuses of resting stops.repair_intent(detail) -> Intent | None: REPAIR after a one-legged fill.square_off_now(now, reason) -> EngineStep: STOP file or manual square-off.size_lots(entry_credit, margin_available=None, margin_per_lot=None, stop_pct=None) -> (lots, math)(stop_pctdefaults to the fixed setting; adaptive entries pass the StopSizer's combined percentage)StopSizer(settings).compute(quote, observation, now) -> StopBasisandStopBasis.to_dict()state_snapshot(): exactly the GET /api/straddle shape (legs carry stop_price, stop_order_id, stop_status, status).status(): state, counters, day P&L, lock state, pending intent.to_trace_step(step, i, *, index, vix, premium, days_to_expiry, stimulus_hash, stimulus_png, rates_hz, fixed_decoder, compute_seconds, technical) -> dict: the api-spec step shape; the caller fills the neural fields.halt(reason),day_pnl(), propertiesin_position,is_halted,is_flat_book,pending_intent,deadline.
EngineStep: at, trigger (observation, tick, broker, manual), action, guard, intents, fills, straddle, pnl_day, narrative, technical, prediction, quote, observation, detail, sizing, closed, entry_failed, notes.
Rules implemented:
- Entry: prediction ENTER, guard allows, FLAT. Sizing
lots = floor(risk_budget_pct percent of capital / (credit x stop_pct percent x lot_size)),
clipped to [1, max_lots], to
strategy.lotswhen it is above 0 (the--lotsrequest is a cap, not a floor) and to floor(margin_available / margin_per_lot) when a margin figure is given. With the market facts (204 point credit, 25 percent stop, INR 10 lakh, 1 percent budget) that is 3 lots. - Combined stop at credit x (1 + stop_pct/100) when
combined_stop_enabled, target at credit x (1 - target_pct/100), lock: once the premium is lock_after_pct below the credit the stop moves to the credit. - Volatility-adaptive stop distances (
stop_modeadaptive, the default).StopSizer(settings).compute(quote, observation, now) -> StopBasisruns at every entry decision: m = max(implied, realized) overstop_horizon_minutes, implied = combined premium x sqrt(horizon / minutes to expiry), realized = std of the trailing log returns ofobservation.index_barsx sqrt(horizon / bar interval) x index (fewer than 20 bars: implied only; returns that span the overnight gap are excluded, so early entries are sized on intraday volatility, not on the opening jump). Per leg the rise for a move m against the leg is |delta| x m + 0.5 x gamma x m squared and the stop percent isstop_bufferx rise / leg price clipped to [leg_stop_min_pct,leg_stop_max_pct]; combined rise = 0.5 x (gamma_ce + gamma_pe) x m squared + |net delta| x m clipped to [combined_stop_min_pct,combined_stop_max_pct]. Delta and gamma are read from optionaldeltaandgammaattributes on the leg Quote objects (the chain resolver may attach them); otherwise ATM defaults apply (delta 0.5 and -0.5, gamma 0.4 / (index x sigma x sqrt(T)) with sigma x sqrt(T) = premium / (0.8 x index), which reproduces the measured gamma 0.00156 for the 204 point straddle). Net delta is the difference of the absolute deltas, so a symmetric straddle has zero net delta. The percentages are fixed for the life of that straddle (never trailed) and recomputed for every new straddle; the combined percentage also sizes the lots.stop_modefixed keepsstop_pctandleg_stop_pct. Thestop_basisdict (mode, horizon_minutes, expected_move_points, implied_move_points, realized_move_points, leg_stop_pct, combined_stop_pct, plus rises, greeks, bars_used, clipping notes) is in the straddle payload ofstate_snapshot(), in every trace step'sstraddleand intechnical.levels; the entry narrative explains it ("Expected one-hour move 73 points (the straddle prices 20, the last hour realized 73). A move of that size lifts the call about 41 points, so the call stop is 50 percent above its price at 152.10; put stop 51 percent at 151.00; combined stop 10 percent at 221.4 (floor 10 percent). Target 120.8, lock after 171.1, hard exit 15:15. Leg stops at the broker, held for the life of this straddle."). With a symmetric straddle the combined rise is gamma-only and small, so the combined stop often sits atcombined_stop_min_pct(3 percent by default so that monthly contracts, whose premium moves a few percent a day, get reachable levels); the leg stops carry the volatility adjustment. - Adaptive target and lock (
target_modeadaptive, the default). The target percent istarget_ratiox the adaptive combined stop percent (after its own clipping), clipped to [target_min_pct,target_max_pct], and the breakeven lock arms once the premium has fallenlock_ratiox that target percent; both are fixed for the life of the straddle and recomputed for the next one.stop_basiscarriestarget_mode,target_pctandlock_after_pct;state_snapshot()target_level uses them and the entry narrative says "Target 4 percent at 468.5, lock after a 2 percent fall (to 470.5)".target_modefixed keepstarget_pctandlock_after_pct. - Per-leg fixed stops at entry_price x (1 + leg stop percent/100) rounded up
to the tick.
leg_stop_modebroker: a STOPS intent (one BUY SL-M leg per option,StopLeg.trigger_price) is emitted right after the entry fills.leg_stop_modesoftware:on_tickcompares each leg's LTP with its stop and emits an EXIT_LEG intent (action STOP_LEG). The stops are never trailed; the lock applies only to the combined stop. - After a leg stop with
on_leg_stophold_other, the other leg keeps its own fixed stop; the combined stop and lock no longer apply (the combined premium then includes the stopped leg's exit price), while the target, readout EXIT and square-off still do. With exit_both the remaining leg is exited at once in the same step. - Early exit on prediction EXIT (exit guard), time exit at square_off minus square_off_lead_seconds (15:14:30 by default) on the first tick at or past the deadline.
- Dynamic straddles: after any exit the engine may enter a fresh straddle at
the strike of the latest StraddleQuote once
reentry_cooldown_minuteshave passed (guard check reentry_cooldown) andmax_entries_per_dayallows (0 means unlimited). Strictly one straddle at a time. The first entry of the day is action ENTER, later ones REENTRY; narratives number them ("Straddle 2 of the day, re-entry after the stop at 10:37: sold ..."). - Expiry selection. The straddle trades the current MONTH expiry by default
(
strategy.expiry_selectionmonthly: the last expiry of the calendar month, 29-SEP-26 in September; weekly selects the nearest Tuesday). The worker asksChainResolver.select_expiry(settings)orexpiry_for_date(day, selection)and passes the result tochain_snapshot(expiry=...); replay-day usesexpiry_for_datefor the replayed date; both fall back to the rule inopenfly/straddle/expiry.py(last Tuesday of the month, moved to the previous trading day when it is a holiday). The guard's expiry_min_dte counts trading days to the selected expiry ("the 29-SEP-26 monthly expiry is 12 trading days away"), the adaptive stops count trading minutes to its 15:30 (375 per session plus the rest of today, never calendar minutes: from 10:20 on 11-SEP-26 that is 4,810 minutes to 29-SEP-26 against 1,060 to 15-SEP-26),expiryandexpiry_selectionare carried instate_snapshot(), in every trace step and in the DayTrace config, and narratives say "the 29-SEP-26 monthly straddle at 23350". Expiry day is allowed when min_days_to_expiry is 0. - An exit whose orders are rejected is retried on the next tick; three failures halt the engine.
- Minimum hold. A readout EXIT is ignored until the straddle has been open
for
strategy.min_hold_minutes(default 10): the step is recorded as HOLD withtechnical.deferred_exit(age, hold_until) and the narrative "The readout wants out but the straddle is only 3 minutes old; holding until 10:30 (minimum hold 10 minutes)."; once the hold has passed an EXIT is taken on the next observation that still says EXIT. Stops, targets, the lock, leg stops and the square-off are unaffected and act at once. The engine status countsdeferred_exits. This answers the first real replay (ten straddles closed by the readout within minutes, costs above gross). - Strike guarantee. The strike of a new entry is computed from the latest
StraddleQuote at that minute (the worker re-resolves the chain snapshot at
every observation while flat; run_day asks the pricer for the ATM when
flat), so straddles re-strike over the day. Once a straddle is open, every
exit (leg stop, combined stop, target, readout EXIT, square-off, repair)
closes exactly the contracts that were entered:
engine.exit_legs()builds the BUY legs from the position's own leg states (the same CE and PE symbols, strike and expiry, with each leg's open quantity), never from a freshly computed ATM; after a leg stop only the remaining leg is included. The dispatcher cross-checks every closing leg againstLedger.open_legs()(net quantity per symbol from the recorded fills) before sending and halts the worker on any disagreement instead of sending the basket. While a straddle is open the worker keeps quoting the open legs, and run_day passes the position's strike to the quote source, so level checks always see the right contracts.
P&L conventions: straddle.pnl on the card is gross (sold value minus
bought value minus the liquidation value of the open legs), matching the
api-spec example. pnl_day is the realized net P&L of the closed straddles
plus the open straddle's gross P&L minus its booked costs. Each closed
straddle is recorded in engine.closed with gross, costs and net.
run_day(date, bars, minute_quotes, brain, encoder, readout, settings, broker, session_window, observation_builder=None, *, interval="1m", vix=None, vix_bars=(), ledger=None, reward_fn=None, reward_offset=0, guard_context=None, trailing_bars=60, engine=None, cost_model=None, on_step=None, render_png=True) -> DayTrace- observes on every completed bar of
interval(375 per day at 1m);barsmay include earlier days, which seed the trailing 60 bar window; - ticks every minute from market open to close plus the square-off
deadline: simulated broker stops (
broker.on_quote) first, thenengine.on_tick, then the observation of a bar closing at that minute; minute_quotes(timestamp, strike=None): when a straddle is open the position's strike is passed so the quotes track the open legs; a one-argument callable is accepted too;- plastic arm: when
settings.neural.plasticis true and a reward function is available (argument, elseopenfly.experiments.reward.reward_for), the reward of the observationhorizon_minutesearlier is delivered as a pulse ("PAM11", 20 x r mV, 200 ms) for r > 0 or ("PPL101", 20 x |r| mV, 200 ms) for r < 0, clamped to [-1, 1]. Never derived from equity ticks.
- observes on every completed bar of
- Helpers:
population_rates,fixed_decoder(DNp20 right minus left, gated by DNpe017, 2 Hz),stimulus_hash,build_observation,days_to_expiry,default_session_window,reward_pulses,interval_minutes. DayTrace(date, steps, summary, config, stimulus_pngs):to_json(),from_json(),save(run_dir)(trace.json plus stimulus/{i}.png when the encoder hasrender_png(stimulus) -> bytes; the step'sstimulus_pngis then the relative path, which the API maps to its URL).- Summary: pnl (net), pnl_gross, costs, trades, entries, stop_hits, target_hits, time_exits, early_exits, stop_hits_leg, vetoes, observations, steps, compute_seconds, halted, open_at_close, closed.
Trace steps: one per observation plus one per tick or broker event that did
something (STOP, TARGET, LOCK, SQUARE_OFF, STOP_LEG, fills). Tick steps carry
trigger "tick" or "broker", the neural fields of the last observation and
technical.observation_i of that observation; prediction is null and
guard is null on steps where the guard did not run.
- Intent kinds: ENTRY, EXIT, SQUARE_OFF, REPAIR, STOPS, EXIT_LEG.
StopLeg(Leg)addstrigger_price.BrokerProtocol: preflight(symbols, lots), execute(intent, quote_lookup), reconcile(), positions(), stop_orders().ClientProtocol: the subset of the OpenAlgo client used (basketorder, placeorder, orderstatus, orderbook, positionbook, cancelorder, analyzer_status; funds, symbol and margin are optional).- Exceptions:
BrokerError,LostResponse,OneLegged,PendingIntentError. EXECUTION_DEFAULTS(read fromsettings["execution"], all optional): order_type LIMIT, limit_offset_ticks 2, fill_timeout_s 10, poll_interval_s 0.5, reconcile_window_s 900, reconcile_interval_s 5, repair_policy unwind, margin_per_lot 190000, tick_interval_s 0.5.quote_lookup_for(straddle_quote),classify_fills,is_balanced,round_to_tick,ceil_to_tick,floor_to_tick.
order_cost_inr(model, side, premium, quantity)andround_trip_inr(model, credit_points, lots, lot_size)adapt any cost model with the openfly.market.costs.CostModel signature (results may be floats or objects with.total).SimpleCostModelis an alias ofopenfly.market.costs.CostModel(one formula everywhere: INR 125.44 for one lot of a 204 point straddle round trip, INR 141.83 for the calculator's buy 100, sell 100, quantity 400).load_cost_model(settings): the market package's CostModel when importable, else the fallback.
Ledger(run_dir, cost_model=None) opens run_dir/ledger.db (WAL,
synchronous FULL). Tables: intents (intent_id, kind, status, created_at,
updated_at, reason, detail, payload json), legs (per intent and symbol:
side, quantity, limit_price, order_id, status, filled_qty, average_price),
fills (idempotent on intent_id, symbol, side, order_id; cost booked with the
cost model), stop_orders (resting SL-M orders with trigger_price, order_id,
status pending, triggered, cancelled, rejected), meta (trading_date,
entries_today, day_pnl, halted, checkpoint).
Methods: reserve(intent) (PREPARED; refuses while another intent is PREPARED, UNKNOWN or ACCEPTED), mark(intent_id, status, detail), status(intent_id), record_leg_order(...), settle(intent_id, fills, status=None) (idempotent; derives SETTLED, PARTIAL or REJECTED when no status is given), add_fills (stop executions), pending(), intents(limit) in the api-spec shape, get, fills, day_pnl() (realized from fills, net of booked costs), halt(reason), halted(), clear_halt(), set_trading_date, entries_today, increment_entries, save_checkpoint, load_checkpoint, record_stop_order, update_stop_order, stop_orders(active_only, symbol), last_stop_for(symbol).
ReplayBroker(cost_model, slippage_ticks=1, tick=0.05): SELL at bid minus slippage, BUY at ask plus slippage, LTP when depth is missing; books costs, tracks positions, holds simulated stop orders and triggers them fromon_quote(quote, now)at max(ask, trigger) plus slippage; cancels stops on the legs before any exit or repair.gross_pnl(),net_pnl(),fills,costs,events,cancelled_stops.OpenAlgoBroker(client, settings, ledger, mode="paper", clock=time.time, sleep=time.sleep):- preflight(symbols, lots): analyzer status matches the mode, funds against the margin (margin endpoint when available, else margin_per_lot), symbol master resolves the legs, no open orders on the legs from other strategies, no pending intent, ledger not halted;
- execute: for exits and repairs cancels the resting stops on those legs
first (a stop found already executed becomes a fill and its leg is
dropped from the basket); marks UNKNOWN before the network call; sends
one basketorder tagged with the strategy with marketable LIMIT prices (LTP
minus or plus offset ticks) or MARKET; inspects results per leg; polls
orderstatus per leg until complete, rejected or cancelled or the timeout;
cancels the unfilled remainder; settles; raises
OneLeggedon an uneven fill (balanced partial fills settle as PARTIAL); - STOPS intents are placed as individual placeorder calls with pricetype SL-M and trigger_price, polled once for an immediate rejection, recorded in stop_orders, and the intent settles SETTLED, PARTIAL or REJECTED;
- reconcile(): PREPARED intents that were never sent become REJECTED;
UNKNOWN intents are matched in the orderbook by symbol, side, quantity,
product, strategy tag (when the entry carries one), not a stop order type,
and timestamp within [created - 60 s, created + reconcile_window_s]; a
unique match per leg is adopted and polled, anything else raises
UnresolvedOrder; ACCEPTED intents are re-polled. Then stop maintenance: a pending stop that completed becomes astop_triggeredevent, a cancelled or rejected stop on a leg that is still short is re-placed (stop_replaced), and an open short leg with no active stop is re-placed from its last record (stop_missingwhen there is none); - positions(): positionbook filtered to the product (non-zero quantities);
- stop_orders(): the ledger's records.
- Helpers
order_state(data)andparse_broker_time(value)normalise OpenAlgo order dicts and timestamps.
execute_step(engine, step, broker, ledger, quote_lookup): reserves, executes and settles every intent of a step in order, including the ones the engine appends while executing (STOPS after an entry, REPAIR after a one-legged fill).OneLegged->engine.repair_intent;LostResponse->broker.reconcile();UnresolvedOrder-> ledger and engine halt and the exception propagates; otherBrokerErrors reject the intent.apply_broker_events(engine, broker, events, now, step=None): feeds reconcile() or ReplayBroker.on_quote() events into the engine and returns the STOP_LEG steps it produced.
Repair policy: "unwind" (default) buys back whatever is short after a one-legged entry; "complete" sells the missing leg. Exits are always completed (the remaining short leg is bought back). A repair that leaves the book unbalanced halts the engine.
Worker(settings, mode, run_dir, brain, encoder, readout, client, feed, calendar, chain, broker, *, ledger=None, cost_model=None, clock=None, sleep=None, history_bars=None, trading_date=None, engine=None, reward_fn=None, interval=None),run() -> int(0 done, 2 halted or refused, 3 lock held). Holdsrun_dir/worker.lock(filelock), refuses live mode without OPENFLY_LIVE=I_ACCEPT_REAL_TRADES, asks the calendar for the window and trading day, waits for the session, resolves the ATM legs fromchain.chain_snapshot()(re-resolved at every observation while flat, so a fresh straddle uses the current ATM), subscribes LTP for the two legs, the index and INDIAVIX, runs the broker preflight, then loops: index ticks build bars ofneural.live_interval(seeded fromhistory_bars()when given), every quote ticks the engine, each completed bar produces an observation,broker.reconcile()runs every reconcile_interval_s, a STOP file in the run directory or its parent squares off and stops, and the loop ends after square-off with a flat book. Every step is appended toevents.jsonl(fsync) as{"type": "step", ...}in the api-spec step shape (log lines are{"type": "log", ...});state.jsoncarries worker, straddle (GET /api/straddle shape), engine status, last_step and preflight.openfly worker --mode paper|live [--lots N] [--run-dir DIR] [--date YYYY-MM-DD] [--interval 5m]openfly replay-day --date YYYY-MM-DD [--encoder B] [--readout reservoir] [--neural-ms 200] [--lots 1] [--stop-pct 25] [--target-pct 40] [--interval 1m] [--out DIR] [--no-png]runsrun_dayoffline with the ReplayBroker and writes trace.json (plus stimulus PNGs) under runs/replays/rp__.
Lazy loaders that fall back to stand-ins so both commands run on a partial
checkout: NullBrain (required population names, zero spikes),
NullEncoder, FixedTimeReadout (ENTER on the observation at trade_start),
simple_minute_quotes (approximate ATM straddle from the index path and a
VIX level), synthetic_index_bars, aggregate_bars.
| Package | Name and call | Used by |
|---|---|---|
| openfly.neural.brain | Brain.from_settings(settings), else Brain.load(graph_path), else Brain(settings); must satisfy BrainProtocol |
factories.load_brain |
| openfly.sensory.encoders | make_encoder(name, settings) -> EncoderProtocol; optional render_png(stimulus) -> bytes |
factories, run_day |
| openfly.readout | make_readout(name, settings) -> ReadoutProtocol (also tried in openfly.readout.readouts) |
factories |
| openfly.experiments.pricer | synthetic_minute_quotes(day, bars1m=, vix=, settings=, expiry=) -> callable(timestamp, strike=None) -> StraddleQuote (positional (day, bars1m, vix, settings) and (day) are tried too) |
replay-day |
| openfly.experiments.reward | reward_for(observation_index) -> float or None in [-1, 1] |
run_day, worker (plastic arm) |
| openfly.market.client | OpenAlgoClient.from_settings(store); basketorder(orders, strategy) returns the results list; placeorder(...) returns {"orderid"}; orderstatus(orderid, strategy) returns the order dict; orderbook() returns {"orders", "statistics"}; positionbook() a list; cancelorder; analyzer_status(); funds(); symbol(); margin(positions) |
OpenAlgoBroker, worker CLI |
| openfly.market.feed | LtpFeed.from_settings(store), start(), stop(), subscribe([(exchange, symbol)]), last(symbol) -> Quote, age_seconds(symbol) |
worker |
| openfly.market.session | SessionCalendar(client, settings), window_for(day), is_trading_day(day) |
worker, replay-day |
| openfly.market.chain | ChainResolver(client, settings, calendar), chain_snapshot() with atm_strike, expiry, rows or atm |
worker |
| openfly.market.history | HistoryCache(client).load(exchange, symbol, interval); BarStore (bars, get or load with exchange, symbol, interval, start, end) is tried first for replay bars |
worker CLI, replay-day |
| openfly.market.costs | CostModel.from_settings(settings) with order_cost(side, premium, quantity) and round_trip(entry_credit_points, lots, lot_size) |
everywhere through load_cost_model |
The replay-day command reads bars from the local store only (BarStore, HistoryCache or the parquet under data/history), never from the broker, and falls back to a synthetic random walk labelled as such in the trace config.
Entry:
10:20. NIFTY 23,350, INDIAVIX 12.1. The readout expects 82 percent of the movement the 23350 straddle is pricing. All 18 checks passed. Sold 1 lot of the 15-SEP-26 23350 straddle for 201.3 points credit (INR 13,085). Stop 251.6, target 120.8, lock after 171.1, hard exit 15:15. Leg stops: call 131.60, put 130.15 (30 percent, at the broker).
Combined stop on a tick:
10:37:12. Combined premium 252.0 reached the stop 251.6. Bought back 1 lot of the 23350 straddle for 252.0 points. Result -INR 3,416 after INR 120 costs. Day P&L -INR 3,416.
Leg stop at the broker:
11:05:00. Call leg stop at 131.60 triggered at the broker. Filled at 132.00 for 65 units. The put stays open with its own fixed stop at 130.15.
Re-entry:
10:45. NIFTY 23,350, INDIAVIX 12.1. The readout expects 82 percent of the movement the 23400 straddle is pricing. All 18 checks passed. Straddle 2 of the day, re-entry after the stop at 10:37: sold 1 lot of the 15-SEP-26 23400 straddle for 200.0 points credit (INR 13,000). Stop 250.0, target 120.0, lock after 170.0, hard exit 15:15. Leg stops: call 130.00, put 130.00 (30 percent, at the broker).
Veto:
14:35. NIFTY 23,350, INDIAVIX 12.1. The readout expects 82 percent of the movement the 23350 straddle is pricing. The guard vetoed: 14:35 is outside the trade window 09:20 to 14:30; 14:35 is past the last entry time 14:30. Still flat.
strategy.lotscaps the risk-based size (min of requested, risk budget, max_lots and margin). Set it to 0 to size purely from the risk budget.- Reconciliation with zero orderbook matches raises UnresolvedOrder (halt for review) rather than assuming the basket was never placed; the plan says never resend.
- Zero-argument
reconcile()also maintains the stop orders; the worker calls it every reconcile_interval_s (5 s). Broker order updates over the websocket are not consumed yet; leg stops are detected by polling. - The STOPS intent is one intent for both legs; if one stop is rejected the intent settles PARTIAL and the next reconcile re-places the missing stop.
- Freeze quantity (1,800 per order) is not split; max_lots 3 keeps orders far below it.
- Re-centering (
strategy.recenter) is not implemented (default off). - The guard's spread check uses the combined premium as the base; with the Friday close quotes in docs/nifty-market-facts.md the call spread is 0.66 percent and would veto at the 0.5 percent default. Live intraday spreads are far tighter; the setting may still need tuning.
- In broker mode the engine relies on reconcile to learn about leg stops; a stop that fires between reconciles is discovered when the exit basket cancels the stops (the fill is then attributed to the stop order).
- The worker builds live bars from LTP ticks; if the history cache is unavailable the first observation happens after the first completed bar.