Version 2.0.2.1 Released
Date: 21st August 2026
Flow Release: takes the no-code builder through four independent QA audits and closes every Critical and High finding, replacing hand-written JSON boxes with typed editors on the Indicator and Margin nodes, repairing logic gates that behaved like the wrong operator, and stopping the editor from silently discarding a user's unsaved graph. Also brings the charting terminal from 20 to 91 built-in indicators, ends the endless "Loading new version" reload loop on nginx installs, and gives migrations the same 15-second SQLite lock timeout the running app has always had
This release spans 25 commits since 2.0.2.0 and is overwhelmingly about Flow. A production QA audit on 21st August 2026 was validated finding by finding against source rather than taken at face value, then remediated across seventeen commits and re-audited three more times; the Critical and High residuals from each round were closed before the next began. The result covers the whole surface: triggers, the scheduler lifecycle, execution reporting, node contracts, the editor, the import format, the generated documentation and a migration that had never actually shipped its column.
Two of the defects fixed here outrank the audit's own Critical list because they lose a user's work: a failed workflow fetch rendered a blank canvas indistinguishable from a new workflow, so the next save overwrote the real graph; and clicking Activate replaced the canvas with the last saved version, discarding unsaved edits with no warning and no undo. A third let Run Now execute a revision the user was no longer looking at.
Outside Flow, the charting terminal picked up 71 more built-in indicators across two openalgo-charts upgrades, the home page and dashboard were reframed around the full platform rather than execution alone, and a run of infrastructure fixes landed on nginx asset serving, migration lock timeouts, the Telegram REST surface, the MCP loopback probe, TradeSmart, Delta Exchange and the Python strategy scheduler.
Highlights
-
Flow: the QA audit remediation (
175124751, #1815) - seventeen commits closing the 2026-08-21 production QA audit and the three re-audits that followed it. Each finding was verified against current source before being fixed: several were overstated, one was materially worse than reported, and six further defects surfaced during the check.Triggers. A one-shot price alert was de-registered on the monitor thread immediately after submitting its worker, so the worker found it gone and dropped the run - measured 40 of 40 dropped once the pool is warm, with only the first alert in a process surviving because
Thread.start()happens to yield the GIL. The run now owns de-registration and claims the alert by identity. A consumed one-shot trigger clearsis_active, so restoring alerts at startup cannot re-arm a spent alert and re-fire its order.restore_price_alerts()was added to mirrorrestore_order_update_watches, since an active price-alert workflow previously went silently dead on restart while the activate endpoint refused it asalready_active. Retirement no longer runs infinally: a one-shot that arrived while a previous run was still in flight used to be consumed and its workflow deactivated without the graph ever seeing the event; it is spent only whenexecute_workflowactually ran. An order update arrives from the bus once and is never replayed, so a refused submit now runs inline on the dispatch thread rather than dropping the fill.Logic gates delivered the wrong answer. A wire into an AND/OR/NOT gate carries the source condition's value, but the graph walk treated it as control flow and filtered it by the branch taken, so a
Falseinput never reached the gate. OR fired only when every input was true (behaving like AND), NOT could never fire at all, no gate could drive its false branch, and the outcome depended on which input the depth-first walk evaluated last - the same graph and the same market data gave different results. Verified over the full truth table in both traversal orders: 16 of 16 OR cases and 16 of 16 AND cases now correct, and NOT(False) fires.The Price Alert node evaluated a vocabulary it does not use. The executor compared the stored condition against the monitor's canonical names only, while the editor writes
above/below/crosses_above/crosses_below. An unmapped name matched no branch, socondition_metstayed false, the order wired to Yes was skipped, and the run still reported completed with no errors - indistinguishable from the level never being reached. A monitor-fired run also no longer re-fetches a quote, since re-checking races the tick that fired it.Order nodes fail on unresolved variables. Interpolation returns the literal
{{name}}text for an unknown path, whichget_intcould not parse and so returned its default of 1, while an unresolvedpriceTypefell through the broker mapping to MARKET. A webhook that simply omitted a key therefore placed a successful order for the wrong size at the wrong price type with nothing in the run to say so. Order-defining fields on the nine order-mutating node types are now checked before dispatch, so the broker call is never made with substituted values.Node contracts aligned with the executor. Modify Order reads the live order and changes only what was supplied - it had been sending hardcoded action BUY, product MIS, price type LIMIT and quantity 1, flipping a live SELL to BUY, NRML to MIS, and silently resizing a 500-lot order to 1. The editor's own
DEFAULT_NODE_DATA.modifyOrdershipped exchange NSE and action BUY, which the executor treats as deliberate overrides, so the node now carries onlyorderId. Close Positions honours its advertised symbol/exchange/product filter instead of always calling the unconditional square-off. HTTP Request parses headers from the JSON string the editor writes (they were silently dropped), supports PATCH, reads timeout as milliseconds capped at 60s, uses its own client rather than the shared broker pool, and rejects non-http(s) schemes plus loopback, private, link-local and reserved destinations - its URL interpolates from webhook-supplied context, so its caller could steer the request. Fund Check and Position Check fail closed instead of guarding against zero or an empty symbol, which had made them always true. Delay is capped at 300s.Condition nodes fail rather than answer from a substituted value.
falseis a real answer that routes the graph down the false branch, so a check that could not run must not produce one.priceConditionvalidated its operator against nothing, read 0.0 for an unknown field, and turned a non-numeric threshold into 0.0; all three now return an error and take neither branch.timeConditionkeeps the seconds it parses - dropping them made "after 15:29:59" fire a minute early whilewaitUntilhonoured the very same string.Execution reporting. The check-then-acquire duplicate-run guard was replaced with an atomic try-acquire, since two simultaneous triggers ran the whole workflow twice. A node returning status error now stops its branch and marks the run failed: a rejected entry leg used to let the hedge leg place and the alert fire, then record the run completed and answer HTTP 200 success. Condition errors are recorded in
executor.errorstoo. Execute and webhook responses mapalready_runningto 409 and failure to 5xx.Scheduler lifecycle. Activation persists
is_activebefore registering and rolls back on failure - it previously registered first and discarded what the database returned, so a failed write reported success while a live persistent job traded against a row marked inactive, unstoppable because deactivate short-circuits and delete only removes the job when active.reconcile_scheduler_jobs()now runs at startup: the jobstore is persistent, so a stale job was restored at every boot and kept trading a workflow the user believed was off. The API key is no longer pickled intoflow_apscheduler_jobs.job_state, where it sat in cleartext in the same database that Fernet-encrypts it; it is resolved per run, so regenerating the key no longer breaks schedules. Editing the trigger of an active workflow re-arms it during the save rather than only reportingneeds_reactivate.Typed fields replace hand-written JSON. The Indicator node asked for its parameters as a JSON string; selecting an indicator now renders its real parameters - number inputs, dropdowns where the SDK documents a fixed set, a switch for booleans, and an explicit "takes no parameters" note for OBV, ADL and the other bare indicators.
INDICATOR_PARAMSis generated from theopenalgo.tasignatures rather than hand-maintained, verified against the live SDK across all 116 indicators and all 36 dropdown values. Switching the indicator now prunes keys the new one does not accept. The Margin Calculator gained a repeatable leg editor whose placeholder JSON could never have worked (it omittedproductandpricetype), with NFO and BFO quantities in lots, and its lot-size lookups batched into onePOST /flow/api/symbol-lotsizesrequest capped at 50 pairs. Storage is unchanged in both cases and raw JSON stays reachable behind a toggle for{{variable}}references.The editor stopped losing work. A failed workflow GET left
isLoadingfalse and the data undefined, so the editor rendered a blank canvas; adding two nodes and saving PUT those two over the real graph and reset the name to the store default. Activate and Deactivate invalidated the workflow query, and the refetch re-ran the hydration effect, replacing the canvas with the last saved graph and clearingisModified- unsaved edits vanished a moment after clicking Activate. A save PUTs the graph as it stood when the request was issued, butmarkSaved()cleared the dirty flag regardless, so the canvas showed B while the server held A and Run Now then executed A;markSavednow takes the revision actually sent andsaveIfDirtyloops until a save completes with the canvas unchanged. Run Now, Activate and Deactivate save first when the canvas is dirty, since they send only the workflow id.Output variables and node subtitles. Nine node types rendered an output-variable name as their input's fallback while saving an empty string, so the box looked filled, the executor stored nothing, and every downstream reference resolved to its own literal text. Basket Order counted
ordersas an array while the editor writes newline-delimited text, and Margin countedpositionswhile the editor writespositionsJson, so both read zero on every node.NodeTypeis now derived from the ReactFlow registry: the hand-maintainedNODE_TYPESwas missing 16 live types and named 10 components that no longer exist, which is what let those count bugs past the compiler.The
api_keymigration had never shipped.create_flow_workflows_tablereturns early when the table exists, so an installation predating the column could never gain it; the only thing adding it was a startup ALTER whose failures were swallowed at debug level, while--statusreported "All changes applied" against a schema with noapi_keyand every activation silently failed to persist. Alongside it,create_executionnow stampsstarted_at- it was only ever set by a status transition nothing performs, so every execution ever recorded had a NULL start time (53 of 53 rows on the live database), and the history query ordering on that column collapsed to insertion order ascending, listing the oldest runs first and showing a workflow's first ever run as its last. Both were drilled against a copy of a real populated database forced back to the old schema.Execution history is bounded.
prune_workflow_executionstrims per workflow toFLOW_EXECUTION_RETENTION_COUNT(default 500) and drops anything older thanFLOW_EXECUTION_RETENTION_DAYS(default 30), running as history is written so the same activity that grows the table trims it. Both Flow monitors now register their shutdown withatexit; neither had a caller anywhere, so the poll thread, the bus subscription and both worker pools survived until the process was killed.Tests and docs. 118 regressions in the audit file and 212 in the backend flow suite, plus a store test driving the real Zustand store for the save race, each verified against pre-fix code rather than assumed.
flow-import-format.mdis authoritative and its 67 JSON blocks all parse; the generated indicator reference emitted PythonFalse/True/Nonerather than JSON, so none of its 116 examples were executable.conftestsetsLOG_DIR=log/test, since errors provoked deliberately by tests were being appended to the operator's productionerrors.jsonl, which is truncated to its last 1000 lines at startup. -
Charting terminal: 20 to 91 built-in indicators (
305c1e737,106e9c151) -openalgo-charts1.1.0 takes the catalogue from 20 to 86 and adds shaded fill regions to 22 of them; 1.2.0 brings it to 91 with CPR and floor pivots across daily, weekly and monthly frames, Seasonality rendered as a table over the chart, AlphaTrend, Range Analysis and WaveTrend Pro. The terminal needed no code changes for either - the indicator layer is registry-driven, so descriptors appear from the version bump alone and five of the new ones render as grouped sections without any work. What did need changing is the menu: 86 entries in a grouped dropdown is a long scroll, so it now has a search box filtering on both display name and id. 1.2.0 also carries a correctness fix for charts already in use: VWAP's session anchor and the daily CPR frame were pinned to an IST calendar day, which is 18:30 UTC and therefore the middle of a US session; sessions are read from the bar gaps now, with NSE behaviour unchanged. -
The endless "Loading new version" reload loop (
2e4584506,bf54acfc3, #1807) - three independent defects that together explain a route flashing "Loading new version" forever and intermittently returning 503, on one browser but not another. The reload guard could never hold:clearChunkReloadFlag()ran at module scope on every page load including the one the reload produced, and since the app shell always mounts and only the lazy route chunk fails, the loop was reload, mount, clear flag, chunk fails, reload. It is now a 30s cooldown that is never cleared, so a second failure inside the window shows the real error while auto-recovery re-arms afterwards./assets/<file>served three representations of one URL unsafely - brotli 33108 bytes, gzip 41166, identity 154440, with the identity fallback carrying noVaryheader while every response is cached immutable for a year, so a shared cache could hand the raw copy to any later request for that URI. The forced upgrade header was then finished off across the remaining nginx config:change-domain.shwould have reinstated the broken block when a user moves domains, and the Ubuntu server design doc's paste-in sample now uses the propermap $http_upgrade $connection_upgradeform. -
Migrations got the app's 15-second SQLite lock timeout (
6bf0db4b0, #1726) -database/__init__.pyregisters a global connect listener settingPRAGMA busy_timeout=15000, which is why the running application waits 15 seconds for a write lock. Migrations never got it:migrate_all.pyruns each script throughsubprocess.run, so every migration is a separate process that imports SQLAlchemy directly and never imports the database package, falling back to the sqlite3 default of 5 seconds. Measured at 5000 with no database import and 15000 after one. A migration therefore gave up three times sooner than the rest of OpenAlgo under identical contention, and an install failed with "database is locked" where waiting a moment longer would have succeeded.upgrade/_pragmas.pyregisters the same listener and is imported by the 21 migration scripts that build their own engine. -
Telegram REST surface repaired (
38f0cacb9, #1577) - every write endpoint on/api/v1/telegramwas broken, and nothing caught it because the/telegramweb UI talks to a different blueprint, so only API users (Excel, Python, MCP) ever reached the code and they got an HTTP 500.POST /startpassed a keywordinitialize_botdoes not accept and then called a method on a name that was never defined or imported anywhere in the module;POST /stophanded a plain tuple torun_until_complete()for a function that is synchronous. Both now use the same calls the working UI route uses, and/notifyis gated. -
MCP loopback health probe honours
MCP_LOOPBACK_URL(45fa60cfb, #1441) - the probe's docstring claimed it resolved its target the same wayblueprints/mcp_http.pydoes, but it never read the variable. That variable is first inmcp_http.py's order precisely because an operator only sets it when neither default answers, which is exactly the topology where the probe then reported a false alarm on/admin. It is tried first now, each target carries its own label, and an unreachable override still falls through. -
TradeSmart: WebSocket lifecycle and rate limits (
16bf8e7f8,9980c48c1, #1805, #1802) - the plugin had drifted from the patterns Shoonya, Flattrade, Zebu and Definedge use, which made an unrelated broker-side problem far noisier and left two real leaks behind. websocket-client 1.9 hands a received CLOSE frame toon_erroras though it were an exception, so every polite server hangup logged an ERROR with "None - None"; close frames are now told from faults and logged at INFO. The heartbeat worker slept withtime.sleep(30), which cannot be interrupted, so every disconnect burned the full join timeout - it waits on a shutdown event now. Rate limits were corrected to the enforced 10/sec and 120/min, and bulk quotes are served from the WebSocket feed instead of REST. -
Delta Exchange: pooled feed kept alive after the last unsubscribe (
28600cc90, #1799) - subscribing to an option chain stopped delivering ticks entirely: the subscription was accepted and logged, no data arrived, and the proxy reported "Stale feed, connected but no ticks". The adapter tore down its own transport once its symbol count reached zero while the connection pool kept holding and reusing the same instance;disconnect()sets_stop_flag, so the retry loop exited permanently and_get_adapter_with_capacity()reused the dead adapter with no health check. Option chains hit this because switching expiry or strikes unsubscribes the whole set before subscribing the new one. The pool now owns the adapter lifecycle. -
Python strategies read session windows from the market calendar (
faeea5de4) - the/pythonexchange selector hardcoded every session window in the frontend, so all four equity and F&O exchanges showed 09:15-15:30. NFO and BFO have run to 15:40 since SEBI's Closing Auction Session moved the cash close to an auction while derivatives kept trading, and the market calendar DB already carried 15:40. The visible label was the smaller half:NewPythonStrategyprefilled the schedule from the same constants, so picking NFO set a stop time of 15:30 and silently cut a strategy off ten minutes before the F&O session ends.GET /python/api/exchangesnow serves the options with their real windows. -
Home page and dashboard reframed (
8697467f7,994db3f1a,83c8d5022) - the hero pill points at Open Varsity, and the "New in V2" tools pill was replaced by a "One platform, many desks" section presenting the unified broker API, the charting terminal, Flow, the options and portfolio suite, the scalping terminal and sandbox testing as one self-hosted stack rather than an execution-only platform. Counts are read from the code rather than copied from the site, so they stay accurate. The dashboard's Telegram Alerts quick-access card now points at Open Varsity. -
Devsprint contributor prep guide (
fa90f09bf, #1804) - a setup guide for developers joining the FOSS United / BangPypers devsprint, covering what the existing docs get wrong or leave out for a first-time contributor: a broker account is only needed to reach the logged-in UI, Node.js is not required for backend or docs work,.envneeds no manual key generation, and whole-repo Ruff is not clean so only touched files should be linted. -
CI flake pinned (
2d8edfd0e) - the third of three calendar integration tests in Strategy Builder was the only one withoutSLOW_INTEGRATION_TEST_TIMEOUTdespite being the same shape as its neighbours; it measured 5078ms against the 5000ms default on the runner and failed the 2.0.2.0 release commit while touching no frontend code.
Dependencies
openalgo-charts: upgraded to 1.2.0 (20 to 91 built-in indicators, VWAP and CPR session anchoring, frontend only)
No Python dependencies changed in this release.