Ten minutes from an empty file to an indicator drawing on your chart. Python runs in a process beside the trading engine, on the same data and the same order path the platform itself uses — so what you write is not a plugin bolted on the side, it is the platform.
Quickstart
Four steps to a live indicator. Everything below this section is reference — you do not need it yet.
1
Open the Code panel
In While25, add the Code panel. It holds your workspace: a file tree on the left, an editor on the right. You install nothing, and nothing leaves your machine.
2
Create indicators/momentum.py
The class declarationdecides the kind — class X(Indicator) is an indicator wherever the file sits. Folders are convention and persistence: files under indicators/, strategies/, monitors/, apps/ and plugins/ are re-registered every time the runtime starts.
3
Paste this
Twelve lines: a rolling return, drawn in its own pane under price.
indicators/momentum.py
import while25
from while25 import Indicator, Plot
from while25.inputs import Int
class Momentum(Indicator):
id = "momentum"
label = "Momentum"
overlay = False # its own pane, below price
inputs = [Int("length", default=14, minimum=2)]
plots = [Plot("mom", color="#e0a34a")]
def compute(self, ctx):
n = int(while25.params.get("length", 14))
rows = while25.data.candles(while25.instrument, while25.interval, limit=n + 200)
if len(rows) <= n:
return None
close = [float(r["close"]) for r in rows]
# Warm-up bars have no value yet — NaN renders as a gap, 0 would be a lie.
out = [float("nan")] * n + [
(close[i] / close[i - n] - 1) * 100 for i in range(n, len(close))
]
self.times = [int(r["time"]) for r in rows] # one timestamp per value
return {"mom": out}
4
Register it, then add it to a chart
Hit Register. It now appears in the chart’s indicator picker like any built-in, and the AI can see it by name.
A strategy is the same shape
Put the file in strategies/, subclass Strategy, and decorate a method with @on.bar(symbol, interval) to run once per closed bar. It stays paper until a human explicitly grants it live.
strategies/breakout.py
import while25
from while25 import Strategy, on
class Breakout(Strategy):
id = "breakout"
@on.bar("EQ:US:AAPL", interval="5m")
def bar(self, ctx, bar):
if bar["close"] > bar["open"] * 1.01:
while25.orders.buy(while25.instrument, 1, stop_loss=bar["low"])
Writing a module
A module is a class. Where the file lives decides what kind of module it is, so an indicator belongs in indicators/ and a strategy in strategies/.
indicators/zscore.py
# indicators/zscore.py
import while25
from while25 import Indicator, Plot, Level
from while25.inputs import Int
class ZScore(Indicator):
id = "zscore"
label = "Z-Score"
overlay = False # its own pane, below price
inputs = [Int("length", default=20, minimum=2, maximum=500)]
plots = [Plot("z", color="#e0a34a")]
levels = [Level(2.0, style="dashed"), Level(-2.0, style="dashed")]
def compute(self, ctx):
n = int(while25.params.get("length", 20))
rows = while25.data.candles(while25.instrument, while25.interval, limit=n + 300)
if len(rows) < n:
return None
close, out = [float(r["close"]) for r in rows], []
for i in range(len(close)):
if i < n - 1:
out.append(float("nan")) # warm-up renders as a gap
continue
w = close[i - n + 1:i + 1]
mean = sum(w) / n
sd = (sum((x - mean) ** 2 for x in w) / n) ** 0.5
out.append(0.0 if sd == 0 else (close[i] - mean) / sd)
self.times = [int(r["time"]) for r in rows] # one per value
return {"z": out}
Folder
Kind
indicators/name.py
Indicator
strategies/name.py
Strategy
monitors/name.py
Monitor
name.py
Analysis — a one-off script
You can predict past the last bar
self.timesmay run PAST the newest candle. Values dated onto the chart’s projected future bars render in the empty area to the right of price, so a next-close forecast draws where it belongs instead of stopping at the last close. A value dated at a time the chart has no bar for — past or future — is simply not drawn.
The package is the API
Inside a running module every call is package-level: while25.candles, while25.buy, while25.subscribe, while25.ui.push, while25.log. There is one module per process, so nothing needs passing around. The ctxa handler receives is that same object — use it when you want the explicit style, or bind a fake with while25.use(fake) in a test. Shorten the name yourself if you like: import while25 as w25.
Everything is synchronous
Each module owns its own process, so a blocking call costs nothing anyone else can see. There is no event loop to yield to — never write async def or await.
Module types
The base class decides what a module may do. All four share the lifecycle hooks.
Base
Purpose
Entry point
Indicator
Returns named series the chart draws.
compute(self, ctx) → dict
Strategy
May place orders, behind the human deploy-confirm.
@on.* handlers
Monitor
Produces verdicts and notifications. Never orders.
@on.schedule
App
Drives a custom panel.
on_start(self, ctx) or run(self, ctx)
PYon_start(self, ctx) · on_stop(self, ctx)
Called once when the module starts and once when it stops. State you set on self persists for the life of the deployment — the runtime reuses the instance rather than rebuilding it per event.
PYrun(self, ctx)
The escape hatch: called once on start, after on_start, for work that has a beginning and an end rather than a handler — a backtest, a report, a one-pass scan. The module is already RUNNING while it executes, so a long job does not leave the panel reading “starting”; raising inside it puts the module in error with the traceback.
PYcompute(self, ctx) → dict | None
Indicators only. Returns {plot_name: values} and sets self.times to matching epoch-millisecond timestamps, one per value — which may extend past the newest candle to forecast forward. Return None to draw nothing. Use float('nan') for a gap — never 0.
Subscriptions
What wakes a module. Declare them as decorators on the methods that handle them.
Declaring subscriptions
import while25
from while25 import Strategy, on
class Breakout(Strategy):
id = "breakout"
@on.tick("EQ:US:AAPL")
def price(self, ctx, tick):
...
@on.fill(tag="breakout")
def filled(self, ctx, event):
...
Decorator
Delivered
Payload
@on.bar(symbol, interval)
yes
One closed bar: symbol, interval, time_ms, OHLC, volume
@on.tick(symbol)
yes
One tick: symbol, ltp, bid, ask, timestamp_ms
@on.schedule(cron=, every=, tz=)
yes
No payload — a wall-clock wake
@on.signal(name, **filters)
yes
Whatever while25.emit sent
@on.news(symbol)
yes
One live news article, as a plain dict
@on.order(**filters)
yes
The whole order lifecycle, fills included
@on.fill(**filters)
yes
Executions only
@on.position(**filters)
no
No publisher — refused at registration
A handler that raises halts the module
Routing stops, on purpose. See Errors for what each failure raises and how to handle it.
Inputs
Declared inputs appear in the chart’s settings dialog and arrive as while25.params. Read them with a default — a plain run passes an empty dict.
A default outside its own bounds raises at import, where you can see it, rather than at run time when nobody is looking.
Plots & style
An indicator declares its shape; While25 renders it. Every value here is a default the user can override in the settings dialog, exactly as for a built-in.
One line. name is both the key compute() returns and the label in the legend. style: solid · dotted · dashed · large-dashed · sparse-dotted plot_type: line · histogram · area · stepline Colours are CSS strings. width 1–4, opacity0–1 — outside those it raises, naming the field.
PYFill(upper, lower, color="", opacity=0.08)
A translucent band between two named plots — a Bollinger envelope.
PYLevel(value, color="", width=1, style="dashed")
A horizontal reference line — RSI’s 30 and 70.
PYBand(top, bottom, color="", opacity=0.06)
Shading between two levels — an overbought zone.
Market data
Served by the engine, which owns the candle cache and the tick buffer. Symbols are always canonical keys — EQ:US:AAPL, FUT:US:MNQ:202609, CRYPTO:GLOBAL:BTC-USD — never a bare ticker.
Closed bars for one instrument, oldest first. source picks which store they come from — the three are different questions, not fallbacks for one another.
Parameters
symbolstrrequired
Canonical key, e.g. EQ:US:AAPL. Not a bare ticker.
intervalstroptionaldefault "1D"
One of 1m3m5m15m30m1h4h1D1W1M. Anything else raises UNKNOWN_INTERVAL.
sourcestroptionaldefault "live"
live reads the in-memory series a chart or an @on.bar subscription is filling. It never fetches, so it raises NO_SERIES when nothing is watching that instrument yet. This is the series an indicator must use — it is the one drawn underneath it. historical is a real vendor pull, cached. intradayis today’s bars straight from the vendor. The SDK refuses an unrecognised value rather than falling back.
startstroptionaldefault ""
ISO date, inclusive. Used by historical only; live ignores it and intraday is always today. Empty lets the engine choose the window.
endstroptionaldefault ""
ISO date, inclusive. Empty means up to the newest bar; the SDK clamps a future date to today.
limitintoptionaldefault 0
Cap on rows, counted from the newest end — asking for 200 gives the most recent 200. 0 means no cap.
Shorthand for source="historical". The engine clamps the window it actually requests to what the vendor serves in one call, so a very wide start returns what is available rather than failing.
Shorthand for source="intraday". Today only, and 1m5m15m30m1h only — anything else raises UNSUPPORTED_INTERVAL.
PYwhile25.data.history(...) → list[dict]
Deprecated alias for candles(...) with source="live". The name promised a fetch the call never performed. Existing scripts keep working; new code should say which source it means.
Response attributes
timeintrequired
The bar’s open time in epoch milliseconds — the same base @on.bar delivers, so live bars line up with rows you already hold.
An instrument nothing is watching raisesNO_SERIES — it does not return an empty list. The engine reads its live bar series and deliberately does not fetch, because a vendor round-trip here would stall every other module.
PYwhile25.data.quote(symbol) → dict | None
The latest quote for one instrument.
Parameters
symbolstrrequired
Canonical key.
Response attributes
symbolstrrequired
The instrument the quote is for.
ltpfloatrequired
Last traded price.
bidfloatrequired
Best bid.
askfloatrequired
Best ask.
openfloatrequired
Session open.
highfloatrequired
Session high.
lowfloatrequired
Session low.
prev_closefloatrequired
Previous session’s close.
volumefloatrequired
Session volume.
timeintrequired
Quote timestamp, epoch milliseconds.
Absent is None, never zero
The whole call returns None when the engine has no data for that symbol. A price of 0is a real price, and a strategy that cannot tell “no data” from “worthless” will act on the difference.
Same row shape as quote(). The SDK omits symbols with no data rather than padding them — so the result may be shorter than the input and you cannot index into it by position.
News articles, oldest first — answered by the engine’s news path, the same one the platform’s news panel reads, so a script can never see a different story than the screen.
Parameters
symbolsstr | list[str]required
One symbol or up to 10 — an 11th raises INVALID.
startstr | intoptional
ISO date or epoch ms; inclusive lower bound. The SDK reads dates and tz-less datetimes as UTC. A range in the PAST works like historical bars: the engine seeks back through the archive to reach it, and returns an empty list — not the nearest articles — when nothing falls inside.
endstr | intoptional
ISO date or epoch ms; a bare date is inclusive to end-of-day.
sourcestroptional
Exact outlet name; empty means all sources.
limitintoptional
Per symbol. 0 → default 50; hard cap 200.
Returns
list[dict]
symbol, time_ms, title, url always; author, excerpt, body, source, sentiment, sentiment_score only when the vendor returned them — an absent field is an absent key, never 0 (a score of 0.0 is a real, neutral reading; test with "sentiment_score" in row, not truthiness). in_market_hours is True/False when the engine could decide against the exchange calendar, None when it could not. The list itself carries two honesty attributes: rows.truncated — the engine hit its page cap while older matching articles may exist — and rows.errors — per-symbol FAILED/PARTIAL codes for vendor fetches that fell over mid-query, so an empty result for a listed key is a vendor failure, never a quiet news day.
A live counterpart: @on.news(symbol)
A module method decorated @on.news(symbol) runs once per live article, receiving the same dict shape — with symbol spelled as the module declared it, and in_market_hoursalways None (live pushes are not session-tagged). Two modules watching one symbol share one upstream subscription; the first to stop cannot cut the second’s feed, and neither can the user closing the platform’s news panel. A refused subscription — the connection’s live-news limit, or a bare symbol the instrument index does not know — logs an error to the console instead of failing silently. Both surfaces need a news provider on the engine — news() raises NO_PROVIDER without one (loudly, because an empty list would look like a quiet news day, which is a different fact), while a subscription goes through and never fires.
Never cached. A stale position or margin figure feeding a sizing decision is a money-path bug, and the round-trip costs microseconds.
PYwhile25.portfolio.positions() → list[dict]
Every open position.
Response attributes
symbolstrrequired
Display symbol.
instrument_keystrrequired
Canonical key.
quantityintrequired
Signed: negative when short.
average_pricefloatrequired
Always per-point. Some brokers report futures average cost as price × multiplier; that is de-normalised at the adapter, so nothing here may scale it again.
Strategies only — and enforced, not advisory: in any other kind (Indicator, App, Monitor, analysis) every order call raises OrderRightsError before a byte leaves the process, and the engine independently refuses a live grant for a non-Strategy. Every call routes through the same OrderInterface the platform itself uses, so risk rules, pre-trade checks and the OMS apply identically — there is deliberately no separate path for Python.
Setting either protective leg makes the group a basket automatically.
stop_lossfloatoptionaldefault 0.0
Protective leg. Either leg alone is valid.
trail_amountfloatoptionaldefault 0.0
Absolute trail distance.
trail_percentfloatoptionaldefault 0.0
Refused on a bracket with BAD_GROUP. The engine’s bracket path carries an absolute distance only, and a percentage needs an entry price to convert — and a market entry does not have one. Use trail_amount.
groupenum (str)optionaldefault inferred
simplebasketocooto. Left empty, the SDK infers it: basket when you set either protective leg, otherwise simple.
linked_order_idstroptionaldefault ""
Required by oco.
parent_order_idstroptionaldefault ""
Required by oto.
tagstroptionaldefault ""
Free label. @on.fill(tag=…) filters on it.
idempotency_keystroptionaldefault ""
Your own de-duplication key; the engine never places a repeat twice.
Response attributes
okboolrequired
Always True here — a refusal raises rather than returning, so there is nothing to check.
order_idstrrequired
The engine’s internal id. This is what cancel and modifytake — not the broker’s.
paperboolrequired
Trueunless a human has armed this module for live through the platform’s deploy-confirm.
broker_order_idsarray (str)required
The broker’s own ids. Several for a bracket — one per leg.
Plus any risk-gate rejection, which arrives the same way. The SDK raises ValueError locally for a bad side or a non-positive quantity, before anything reaches the engine.
Amend a working order. The engine leaves any field you set to 0 unchanged.
Placing a bracket
import while25
from while25 import While25Error
try:
res = while25.orders.bracket("FUT:US:MNQ:202609", "BUY", 1,
take_profit=21_450.0,
stop_loss=21_380.0,
tag="breakout")
except While25Error as e:
# A refusal RAISES — it does not come back as {"ok": False}. Risk-gate# rejections arrive here too.
while25.log(f"refused [{e.code}] {e.message}", level="error")
else:
if res["paper"]:
while25.log("filled on the paper book") # not armed for live
else:
while25.log(f"live order {res['order_id']}")
Live requires an explicit human grant
Every result carries paper. It is trueunless a human has armed that module through the platform’s deploy-confirm — so a module’s orders go to the paper book even when the platform itself is live. This is fail-closed and nothing a script writes can change it.
Prefer a bracket to a naked entry
A bracket is engine-owned, persisted, and restored at boot — it survives the script’s process dying. A stop managed in Python does not.
Chart
Series and drawings on the chart the user is looking at.
Draw a series from a strategy or monitor. An indicator does not call this — it returns a dict instead. There is no color argument; you declare the style.
A level, trendline, zone or label. anchors is a list of (time_ms, price) pairs — the same units history() returns, so a level anchored to a bar you just read needs no conversion.
PYwhile25.chart.erase(name="")
Remove one drawing, or every drawing this module made.
PYwhile25.chart.tools() → list[dict]
Every drawing tool the chart knows, with anchor requirements and config schema. A tool added to the platform later appears here on its own. The current tool_typevocabulary — verify anchors and config with tools() rather than guessing: lines horizontal_line vertical_line trend_line ray extended_line horizontal_ray; channels parallel_channel regression_channel pitchfork; shapes & zones rectangle ellipse triangle arrow polyline price_zone; ranges price_range date_price_range ruler; text & markers text_label anchored_text price_label callout comment signpost sticker arrow_marker; fibonacci fib_retracement fib_extension trend_fib_extension; positions long_position short_position risk_reward; patterns xabcd_pattern abcd_pattern elliott_wave elliott_abc; volume & VWAP anchored_vwap volume_profile_range volume_profile_anchored.
Drawing the same name again moves that drawing rather than adding a second one — the same rule plot() follows. A level that walks with price is one drawing that moves, and re-running a module replaces its drawings instead of duplicating them.
Visualization
Publish a view to the Wiz panel. rows is a list of dicts or a pandas DataFrame. Name the encoding — While25 infers axes from column types when you leave them blank, which is fine for a table and a coin toss for anything else.
while25.viz.bar(trades, "P&L by symbol", x="symbol", y="pnl")
while25.viz.heatmap(grid, "Correlation", x="a", y="b", value="rho")
while25.viz.table(df, "Open positions") # a DataFrame works directly
The SDK refuses a missing role rather than guessing
A treemap has no area without value and a combo has no second axis without y2, so the SDK rejects those with a message naming the field — rather than rendering a picture that is quietly wrong.
Panels & UI
An App owns a CUSTOM PANEL, a tab that names the app the way a chart tab names its instrument. Saving the file under apps/ is what registers it: the tab appears under VIEW → Custom panels and survives restarts, reloads and a synced layout on another machine.
Start, Stop, Restart and Remove live on the panel itself, because the panel is the application's source of truth. Close stops the app and closes the tab without touching the file, and deleting the file is what removes the panel for good.
The module declares its layout once, at import, and afterwards only pushes values into named slots. While25 owns every pixel.
apps/desk.py
# apps/desk.py
import while25
from while25 import App, on
from while25 import ui
ui.panel("desk", "Trading Desk")
ui.layout(
ui.row(ui.metric("pnl", "Day P&L"), ui.metric("pos", "Positions")),
ui.chart("equity", "Equity curve"),
ui.table("fills", "Recent fills"),
)
class Desk(App):
id = "desk"
@on.schedule(every="5s")
def refresh(self, ctx):
p = while25.portfolio.snapshot()
while25.ui.push("pnl", p.get("day_pnl"))
while25.ui.push("pos", len(while25.portfolio.positions()))
The four built-in leaf widgets. Each owns a named slotyou push into. A chart slot fed a Wiz-shaped spec renders the platform’s interactive chart surface — hover, tooltips, legend. Declare live=True only when the chart redraws at data rate: that buys a lightweight SVG fast path showing axis min/max and the x extent instead of full interactivity. The default is right for anything a person reads.
PYui.view(slot, label="")
The platform’s own components, when you would rather not write markup (and the backtest view). Push a dict of blocks describing what to show — header, tiles, kv, table, badges, text, progress, chart, html, section, row, divider — and the While25 renders it in its own components. A chart block is the interactive Wiz surface by default (any of the 17 kinds; "live": true for the SVG fast path), and an html block is your own markup as one sandboxed section — so custom HTML and an interactive chart share one panel, side by side in a row.
PYui.html(slot, label="")
The app’s own page, and the default for a panel — a dashboard, a desk, a styled report, a custom grid. Push a markup string; your markup and CSS render as written, charts included (inline <svg>or CSS bars). Declare it as the layout’s only widget and the view becomes the whole panel (full height, no caption, no padding); put it alongside other widgets and it is one content-sized section among them, its height tracking its own content. label is optional here — pass one for a caption, omit it and your own <h1> leads.
PYthe frame you render into
Not a bare iframe. The platform’s theme tokens resolve inside — var(--color-amber), --color-profit, --color-loss, --color-text-dim, --color-surface-2 and more — and follow a theme switch. Bare markup arrives styled (headings, zebra tables with tabular numerals, code blocks, lists), with helper classes .card, .tile, .grid, .row, .num, .profit, .loss, .muted. Three faces by role — IBM Plex Mono for data, Inter for prose, JetBrains Mono for code — on a four-step scale. That brand theme is the default, not a rule: it sits in a lower cascade layer, so any CSS you write wins over it, and the platform never snaps or strips anything you write. Build on the tokens and classes and the page reads as part of the platform. Links open in the system browser (https only).
HTML and CSS — scripts never run
The fragment renders in a sandboxed frame with no script permission: <script>, on* handlers and javascript: URLs are inert, so an onclick would silently do nothing. Compute in Python and push new markup instead. CSS animations, transitions and :hover do work — that is the motion you have. The frame isolates styles both ways, and vh units resolve against the frame rather than the window.
PYwhile25.ui.push(slot, value)
Send a value to a declared slot. If you push to a slot you never declared, the SDK logs it and drops it rather than raising — a typo in a display label should not halt a module.
Display only, for now
metric, chart, table, text and html show what a module computed. None of them accept input or call back into Python. The interaction model is a deliberate separate design pass — freezing a half-considered callback contract that third parties must live with would be worse than waiting.
Workspace
Your modules, your data, and your packages live in one directory. A module runs with it as the working directory, so every path below is relative — the same script then runs unchanged against a cloud volume.
the workspace
<workspace>/
├── indicators/ strategies/ monitors/ apps/ your modules
├── data/ files you drop in
├── lib/ packages you install
└── requirements.txt
PYopen("data/name.csv")
Drop files onto the workspace drawer in the Code panel and they land in data/. When a name already exists, the platform adds a suffix and never replaces the file — silently overwriting a file a running script is reading would change its results with nothing to notice.
PYPackages
numpy and pandas ship with the platform. Anything else — scipy, scikit-learn, xgboost — you install from the same drawer, or by listing it in requirements.txt. They go to lib/, which sits on sys.path ahead of the bundled copies, so a version you pin wins rather than your install quietly doing nothing.
Opening a folder runs nothing
The Code panel opens any folder or file on your machine, the way an editor does. Nothing in it executes until you press Run — and a file you run from an opened folder runs once and the platform never registers it, so it cannot become a panel, a strategy or anything the terminal restores at boot. Save As into the workspace is what makes a file part of the terminal; the path alone decides.
Context
Everything else on while25. The ctx a handler receives is the same object, kept for the explicit style and for tests.
PYwhile25.instrument · while25.interval
What the chart asked for. An indicator should compute from these rather than hardcoding a symbol — one registration then serves every chart.
PYwhile25.params → dict
The user’s values for your declared inputs.
PYwhile25.log(message, level="info")
Writes to the panel console. level is info or error.
PYwhile25.emit(name, payload=None)
Fire a signal another module can receive with @on.signal(name). Fire-and-forget: no queue, no replay, and a signal with no subscriber is a no-op.
Run another indicator in this process and get its series back — no chart, no round trip. This is how a strategy uses an indicator’s numbers.
PYwhile25.alive() → bool
False once the platform asks the module to stop. Check it inside any long loop.
Errors
Every while25.data, while25.portfolio and while25.orders call is a round-trip to the engine, and a refusal raises — it never comes back as a result object with ok=False. There is nothing to check on the happy path.
Handling a refusal
import while25
from while25 import While25Error
try:
rows = while25.data.candles("EQ:US:AAPL", "5m", limit=200)
except While25Error as e:
if e.code == "NO_SERIES":
while25.log("not being watched yet — open it on a chart")
return
raise # anything else is not ours to swallow
PYWhile25Error(RuntimeError)
.code is the machine-readable string in the table below; .message is the prose. Branch on the code — message wording changes, codes do not. It subclasses RuntimeError, so an existing except RuntimeError still catches it.
Code
Raised by
Cause and what to do
NO_SERIES
while25.data.candles (source="live")
Nothing is watching that symbol at that interval, so there is no live bar series to read. source="live" deliberately does NOT fetch — it exists to return exactly the bars a chart is drawing. Open the instrument on a chart, subscribe it with @on.bar, or ask for source="historical".
UNKNOWN_INTERVAL
while25.data.candles
The interval string is not one the engine recognises. Use "1m", "5m", "15m", "1h", "1D" and friends — retrying will not help.
NO_DATA
while25.data.candles (fetched)
We asked the vendor and it returned nothing for that instrument and window. Distinct from NO_SERIES: the fetch happened, the data does not exist.
BAD_RANGE
while25.data.candles (source="historical")
start is after end. Both are ISO dates; the SDK clamps a future end to today rather than refusing it, so this only fires on an inverted range.
UNSUPPORTED_INTERVAL
while25.data.candles (source="intraday")
Intraday serves 1m, 5m, 15m, 30m and 1h only. Ask for source="historical" for anything coarser.
NOT_READY
any while25 call
The engine is still assembling that subsystem — market data, the portfolio, or the order path — usually within seconds of start-up. Transient: retry on the next event.
NOT_READY
while25.orders (bracket)
The bracket path is unavailable, so the engine REFUSED the entry rather than placing it naked. This is deliberate: a protected order silently becoming unprotected is worse than no order.
BAD_ORDER_TYPE
while25.orders.place
order_type is not one of the accepted values. Your input is wrong; retrying will not help.
BAD_PRODUCT
while25.orders.place
product is not one of the accepted values.
BAD_VALIDITY
while25.orders.place
validity is not one of the accepted values.
BAD_GROUP
while25.orders.place
The group type is unknown, or the group is missing a field its type requires — an OCO without its linked order, a bracket with neither leg priced.
SIDECAR_DOWN
any while25 call
The Python process is not running, or the engine could not reach it. Restart the module.
Raised without a code
These come from the runtime rather than from an engine refusal, so they carry no code.
Exception
When
ValueError
while25.orders raises this LOCALLY before it contacts the engine: a side that is not BUY/SELL, or a quantity that is not positive.
TimeoutError
The engine did not answer within the call timeout. The script is not wedged — it was waiting, which is why this is not a hang.
RuntimeError
The platform is not reachable at all: the pipe to the engine is gone.
A handler that raises halts the module
An uncaught exception stops routing for that module, deliberately: a strategy that raised part-way through an entry is holding a position it has lost track of, and the next event would run the same handler again. The process stays alive so the traceback is readable; restarting clears it.
The SDK itself has no dependencies, so you can write and unit-test a module on a plain Python install before it ever touches the platform. numpy and pandas are available in the runtime. Get the desktop app to run it — Python runs beside the engine, which a browser cannot host.