Skip to content

Serverless Trading System simulation built with AWS, Node.js and TypeScript.

Notifications You must be signed in to change notification settings

NicolaNardino/serverless-trading-system

Repository files navigation

Welcome to serverless-trading-system

It covers the front-to-back high-level architecture of a trading system, from when an order gets entered, from a Broker UI, to the trade settlement, passing through simulated Dark and Lit Pools matching engines. The focus is on the overall infrastructure rather than on the actual matching engines.

It's a generalization of my previous project, TradingMachine.

Main technologies used:

  • Node.js, TypeScript.
  • AWS: Lambda & Lambda Layers, Step Functions, API Gateway, EventBridge, SNS, DynamoDB & DynamoDB Streams, S3, Parameter Store. Configuration and deployment of all resources by a SAM template.

The code layer had initially been built with Javascript ES6, and later migrated to TypeScript. The left-over JavaScript ES6 code, containing some features not migrated to TypeScript, is within the folder serverless-trading-system-stack/src/lambdas/legacy.

Software Architecture

ArchWithStepFunctions

Initially, it was designed with SNS as message bus, then replaced with EventBridge. The application is able to work with both message buses, in fact, it's possible to switch between them by the means of a AWS Systems Manager Parameter Store param, /serverless-trading-system/dev/bus-type, whose values can be SNS or EVENT-BRIDGE. For straight pub/ sub use cases, the EventBridge client/ service programming model matches almost 1:1 the SNS one, for instance:

  • SNS subscriptions --> EventBridge rules.
  • Similar client-side API.

With the EventBridge, source events can be modified before getting to consumers, for instance, by removing the event envelope, so to have a boilerplate-free events retrieval code, for instance, in the target Lambdas.

Rule example:

{
  "detail-type": ["Orders"],
  "source": ["SmartOrderRouter"],
  "detail": {
    "PoolType": ["Dark"]
  }
}

Part of the matched event target deliver:

$.detail.orders

Where detail is the event envelope. In this way, only the array of orders will be delivered to the target. Compare that with the boilerplate code require in a SNS subscriber.

Step Functions

Step Functions are used to deal with retrieving market data from Yahoo Finance. Specifically, one is exclusively used in the context of the order workflow to retriave market data (quote summary and historical data) for order tickers. This one, triggered by an EventBridge event, uses a Parallel state branching out to two lambdas, each specialized either in quote summary or historical data. See the overall software architecture.

The other one can be triggered by an API Gateway post end-point or directly through the Step Function APIs. The latter allows it to be called in the order workflow too. It uses a single lambda to retrieve both quote summary and historical data. It gets executed in parallel via a Map state. The overall execution is asynchronous, given that the Step Function uses a standard workflow, which contrarily to the Express one, doesn't allow synch executions. In order to allow further processing, at the end of each successful market data retrieval, the state machine emits an EventBridge event. In case of failure retrieving market data, it enters in a wait state (waitForTaskToken) and delegates the error management to a Lambda function outside the state machine. When that finishes it calls SFNClient.SendTaskSuccessCommand(...taskToken) to let the state machine resume and complete its execution.

Below is a state machine extract that defines a lambda state as "waitForTaskToken", and then allows for the token to be obtained by the Lamnba itself in its payload.

"No historical data retrieved": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
      "Parameters": {
        "Payload": {
          "input.$": "$",
          "taskToken.$": "$$.Task.Token"
        },
        "FunctionName": "${NoHistoricalDataRetrievedLambda}"
      },
      "End": true
    }

market-data-manager-step-function-api drawio (1)

Order flow

Orders get into the trading system through a POST endpoint, with the following structure:

{
      "customerId": "000002",
      "direction": "Buy",
      "ticker": "COIN",
      "type": "Limit",
      "quantity": 8614,
      "price": "161.90"
 }

And get out so:

{
    "customerId": "000002",
    "direction": "Buy",
    "ticker": "COIN",
    "type": "Limit",
    "quantity": 1000,
    "price": "161.90",
    "orderId": "a62f7393-8d7e-46f7-a014-222e286c092b",
    "orderDate": "2022-01-31T17:41:34.884Z",
    "initialQuantity": 8614,
    "split": "Yes",
    "tradeId": "e518d027-3cea-45f0-b661-e56b71b0dfa5",
    "exchange": "EDGA",
    "exchangeType": "LitPool",
    "tradeDate": "2022-01-31T17:41:35.619Z",
    "fee": "0.58",
    "settlementDate": "2022-01-31T17:41:39.192Z"
}

According to the following order flow:

OrderFlow (4)

DynamoDB Data Layer

There are 3 entity tpyes:

  • Customers, CUST#cust-id: their initial data get entered at system initialization time, then enriched with stats during the data aggregation step. customer-init At data aggregation time, new attributes are added/ updated (TotalCommissionPaid, NrTrades, TotalAmountInvested, Updated) and exixting ones (RemainingFund) updated. customer-update

  • Trades, TRADE#trade-date#trade-id: they are in a 1:n relationship with Customers. trade

  • Tickers, TICKER#ticker-id: they are an outcome of the trade aggregation step, where trade data get aggregated at ticker level. ticker

Notes

Node.js ES6 modules

By using Node.js ES6 modules, it's possible to let the Lamba wait for its initialization to complete, i.e., before the handler gets invoked:

const paramValues = new Map((await ssmClient.send(new GetParametersCommand({Names: ['/darkpool/dev/order-dispatcher-topic-arn', '/darkpool/dev/darkpools']}))).Parameters.map(p => [p.Name, p.Value]));
...
export async function handler(event) {...}

See here.

Lambda Proxy Integration

The 2 API Gateways, SmartOrderRouter-API & DataExtractor-API, use the Lamba Proxy Integration, i.e., /{proxy+}.

data-access-layer

Lambda Layer

lambda-layers

In a real-world project, I'd have split the common code in multiple layers.

Market Data

Yahoo Finance is the data source for market data, ticker (quote) summary and historical data. I'm targeting to assess customers' portfolios at given dates.

Dark & Lit Pools

While Lit Pools are usually known by the broader audience, in fact, they're the commonly known Stock Exchanges, the same can't be said about Dark Pools.

TODO

  • JavaScript to TypeScript conversion.
  • Manage order workflow through state machines/ step functions.
  • Add OpenAPI specs.