developer-experience
editor
monaco
tooling
quant

An Editor That Writes Itself: Autocomplete Derived Straight From the Engine

Jonny Bravo
  -  

...

An Editor That Writes Itself: Autocomplete Derived Straight From the Engine

There's a special kind of betrayal in a code editor that autocompletes a method that doesn't exist. You trust the dropdown, you pick the suggestion, you run it, and it blows up — because the autocomplete was hand-maintained on the frontend and drifted from the backend three releases ago. Now you trust nothing the editor tells you, which means the editor is doing negative work.

This is the default fate of any in-app code editor. The authoring surface lives in the backend (the engine); the autocomplete data lives in the frontend (a TypeScript file someone maintains by hand). The two drift apart the moment anyone forgets to update both. And someone always forgets.

We refused to accept the drift. The strategy editor's autocomplete is derived from the engine's own source code, computed on demand. The surface you're offered is, by construction, the surface that actually runs.

The problem with hand-maintained completion

When you write a strategy in the browser, you want the editor to know what self. offers, what an indicator's signature is, what members a snapshot has. The naive way to provide that is to write it all out in the frontend — a big object listing methods, params, and types — and keep it in sync with the backend by discipline.

Discipline doesn't scale across a codebase and a team. The backend adds a self.funding() helper; the frontend doesn't hear about it; the editor never offers it, or worse, still offers the old self.funding_rate() that was renamed. Every divergence erodes trust until the autocomplete is decoration.

Introspect the source, don't transcribe it

Instead, the editor asks the backend a single question — "what can I write?" — and the backend answers by introspecting the real modules rather than reading from a hand-kept list. From the API-catalog module's own docstring:

"""Source-derived authoring-API catalog for the strategy editor. Rather than hand-maintaining that list on the frontend (where it drifts from base.py), this module introspects the real modules with `inspect` and emits a JSON-serialisable catalog. The set of modules is exactly the sandbox's ALLOWED_IMPORTS for the fund package, so "what can I import / call" is answered straight from source and stays in lockstep with the engine."""

Two things make this airtight:

  1. The module set is the sandbox allowlist. The catalog introspects exactly the modules a strategy is allowed to import. So "what the editor offers" and "what the sandbox permits" can't disagree — they read from the same list.

  2. The receiver types are the real classes. When you type snapshot. or self., the editor resolves members from the actual engine classes:

RECEIVERS = { "snapshot": "MarketSnapshot", "state": "PortfolioState", "self": "SingleInstrumentStrategy", "TargetPosition": "TargetPosition", }

It walks those classes with inspect, reads their methods and signatures, and ships them to Monaco. Rename a method in base.py and the next catalog request reflects it. There is no second copy to update because there is no copy at all — just live introspection of the source of truth.

The whole thing is exposed as one endpoint, GET /v1/fund/strategies/api-catalog, and cached, so the editor stays fast.

Mark a method as an override point — and it appears

The most elegant part is how authoring hooks surface. A strategy's lifecycle methods — the ones you're meant to override — are tagged in the engine with a single decorator:

@overridable def data_requirements(self) -> List[DataRequest]: """Logical datasets consumed. Default: OHLCV bars only.""" return [Bars("ohlcv", interval=self.bar_frequency_seconds)]

The catalog reads these @overridable markers and synthesizes ready-to-fill stubs from the live signature, so typing def data_require… in the editor offers a real def data_requirements(self): skeleton — generated from the function as it exists right now, not from a description of it. The decorator's own docstring states the payoff: "Adding a new hook is just one @overridable here; no frontend change."

That's the whole design philosophy in one sentence. A backend engineer adds a capability and decorates it; the editor offers it to every user on the next request; nobody touches the frontend. The handful of cases where the real signature isn't the one authors should type — on_bar(self, *args, **kwargs) should present as def on_bar(self): — are handled by passing an explicit override on the decorator, kept right next to the method it describes:

@overridable(signature="(self)", body="self.target(weight=1.0) # untouched instruments are HELD") def on_bar(self, *args, **kwargs): ...

Why developer experience is a sales feature

It's tempting to file "good autocomplete" under nice-to-have. It isn't. The editor is where a prospective customer forms their first, fastest judgment of whether your platform is serious. An editor that suggests phantom methods says "this was thrown together." An editor that knows the exact, current API — that completes self.funding() because the engine grew it yesterday — says "the people who built this sweat the details."

And it compounds. Every authoring improvement in the engine propagates to the editor for free, forever, with no frontend coordination. The platform gets more trustworthy as it evolves, instead of accumulating drift. For a quant evaluating where to build their book, that reliability is the quiet thing that tips the decision.

The best editors feel like they were written for the exact code you're writing. Ours was — it reads that code to build itself.


Type a strategy and watch the editor complete the real engine API, signatures and all. Try the editor →

Article Contents

Related Articles

© 2022 Fluxy, Inc. All rights reserved.