As first published here by Cord on 16th October 2024
At OptAxe, we’re building a trading venue for one of the most complex financial instruments available to trade - FX Options. For the uninitiated, an option is a contract that gives the owner the right, but not the obligation to buy some amount of currency
at a future date at a pre-agreed rate. Without digging too deep into what makes up one of these contracts, there are clues in the above sentence that adding one of these highly customisable products onto our platform is going to involve fetching market data - we’re talking interest rates, volatility metrics and expiration dates. In fact, lets take a look at some unnecessarily complex maths to better illustrate whats going on:
Thankfully, the development team at OptAxe don’t have to understand the workings of this equation, but we do need to provide the inputs. Fetching the interest rate, time to expiration and ultimately the price of the option involves three calls (plus any additional calls for incident management) for a single leg structure, each with multiple inputs. When you factor in that the returned data might be needed in the body for subsequent requests, across a multi-leg structure* (as you see above) its easy to see how the complexity of fetching and resolving this data in a structured way can get tricky. Additionally, many of our users (banks and hedge funds) will use their own internal models for pricing options which necessitates some additional flexibility so that they can use our data or use their own.
So the team looked for a solution that could manage these requests, provide the flexibility that our clients expect, whilst leaving React to update the UI to keep our forms snappy.
Web workers and managing requests
The figure above is a simplified illustration of what a multi-leg fixture can look like, but In reality we’re dealing with ~90 inputs! This forms complexity isn’t just down to the number of fields however, there is also the business logic that ties them together. Its possible that a single update to an input may trigger multiple value updates, and these fields may initiate market data requests. To see how this could go wrong, lets imagine that we fetch on keypress. Disregarding whatever computationally expensive work may interfere with the UI, the requests will resolve and we can’t guarantee the order.
To handle this complexity we offload value changes and decision-making to a web worker. The web worker asks:
Do we have enough form data to call the market data service?
Does the form data differ from the cached values we kept from the previous request?
Are there in-flight requests and if so, do they need to be cancelled to allow a refetch with newer values?
To execute this logic, we pass each value change to our worker where the form state is compared against our in-worker cache of the previous state. If there is a change, we assign a type signifier that tells us where the request is being sent, and we identify it within the [Map] of our controller class by an ID/[AbortController] key/value pair. The abort signal for every newly instantiated [AbortController] is then passed to our fetch client. This is handy as the [Map] preserves our insertion order, and we can easily find and cancel incoming requests. When a new value comes in, we simply lookup the in-flight requests and if needed, cancel them before resolving. This allows us to return only the data for the latest user input.
Handling custom inputs and form types
Traders will sometimes use our data as a reference and use their own values that come from their proprietary pricing models. This means that when a user states one of these values, we need to ensure their custom inputs persist across the form. To handle this, we maintain a state object that keeps track of values users want to keep with a context layer called the [InputStateContext]. This then becomes an input into our worker and helps form the cache which prevents new requests being triggered when user-inputted data should persist.
All these elements - The form values, overtyped state, fetching status and retrieved data are then funnelled into the Form Data Processor (FDP). The FDP handles formatting and on-update business logic. This allows us to compute a new form values object to populate the form on a single state update. Of course, there isn’t just a single form type to deal with(!) so each form has an associated configuration object that can be easily consumed by the FDP for different logic requirements.
What this leaves us with is a satisfying separation of concerns.
We let a customisable FDP layer handle update logic for different form types.
We have a robust data manipulation and data fetching solution within our web worker that processes requests neatly away from the main thread
We maintain a context layer for storing additional input state.
As a result, our trading experience can stay smooth and responsive—even under demanding conditions.
In the coming months, as we push for launch and the product matures, this approach will make it easier for our dev team to deliver features efficiently, identify and resolve bugs quickly, and write robust tests that will hopefully set us up for long term success.
*Sometimes traders want to use two or more individual options to manage risk or to enhance profit potential. These are called multi-leg options, and include all the fields from a single-leg option but repeated in up to 3 legs on the OptAxe platform.