Skip to content

About

A highly scalable backend service for a modern wealth management platform. This service receives a total investment amount and a model portfolio, and splits the total amount into individual stock orders based on target allocations.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Robo-Advisor Order Splitter

A NestJS and TypeScript proof-of-concept API for splitting model portfolios into executable stock orders.

Features

  • Order Splitting: Accurately divides an investment amount across multiple stocks based on percentage allocations. Dollar allocations are strictly reconciled down to the cent using the largest-remainder method, ensuring 100% accurate split sums.
  • Transparent Ledger: Elegantly solves the fractional share "dust" problem by decoupling targetAmount from the strictly-calculated executableAmount, returning unallocatedCash at the parent level to guarantee airtight financial invariants (sum(targetAmount) = totalAmount and sum(executableAmount) + unallocatedCash = totalAmount). The API intentionally and conservatively rounds each executableAmount down to two decimal places to ensure costs never exceed the user's allocated budget. For SELL orders, unallocatedCash represents the notional value that could not be sold because of fractional-share precision limits.
  • Fractional Shares: Calculates precise fractional share quantities, handling truncations to avoid over-investing (default: 3 decimal places).
  • Execution Scheduling:
    • Immediately executes orders during US market hours (9:30 AM – 4:00 PM ET, Mon-Fri).
    • Schedules orders outside market hours for the next open market time using an in-process scheduler.
  • RESTful API: Built with NestJS, featuring robust validation and error handling.
  • In-Memory Storage: Uses an in-memory repository (Map) for fast development and testing (data does not survive restarts, as per requirements).
  • Event-Driven Architecture: Uses EventEmitter2 for internal domain events (order.created, order.executed, order.scheduled), paving the way for distributed messaging (Kafka/RabbitMQ).
  • Swagger Documentation: Interactive API documentation available at /api/docs.

Technology Stack

  • Node.js (v20+)
  • NestJS (v11) - Enterprise-grade TypeScript framework
  • TypeScript - Strict typing
  • class-validator / class-transformer - DTO validation
  • decimal.js - Robust financial mathematics
  • Jest - Unit and E2E testing
  • Swagger - OpenAPI documentation

Assumptions & Design Constraints

  1. Default Stock Price: The default price of any stock is $100.
  2. Price Overrides: Partners can pass an optional price override for any stock in the portfolio, which takes precedence.
  3. Market Hours: Standard market hours are Monday-Friday, 9:30 AM to 4:00 PM Eastern Time.
  4. Market Holidays: For simplicity in this PoC, specific market holidays (like Christmas or Thanksgiving) are not modeled.
  5. Fractional Math: Quantities are truncated (not rounded up) to prevent the sum of fractional shares from exceeding the user's allocated budget.
  6. Data Storage: Data is stored in-memory and lost upon restart.
  7. Authentication: No authentication is included because this is a PoC, though an accountId context is passed to simulate multi-tenant B2B environments.

⚙️ Configuration

The application can be customized using the following environment variables:

  • PORT: API port, default 3000
  • QUANTITY_DECIMAL_PLACES: fractional-share precision, default 3, maximum 10
  • DEFAULT_STOCK_PRICE: fallback stock price, default 100

Example:

QUANTITY_DECIMAL_PLACES=7 DEFAULT_STOCK_PRICE=100 npm run start:dev

🚀 Getting Started

Option 1: Local Development

Use this option if you want to run the API directly on your machine.

npm install
npm run start:dev

The API will be available at http://localhost:3000.

Option 2: Docker

If you prefer not to install Node dependencies locally, you can run the app with Docker Compose:

docker-compose up --build

The API will be available at http://localhost:3000.

Interactive API Testing (Swagger)

The easiest way to test the API interactively is through the included Swagger UI. Once the server is running (either locally or via Docker), open your browser and navigate to:

👉 http://localhost:3000/api/docs

Here you can explore the endpoint schemas and click "Try it out" to send live requests directly to the API without needing Postman.

Running Tests

The repository includes unit and e2e tests for the main business rules and API behavior.

npm test
npm run test:e2e
npm run test:cov

📖 API Examples

1. Create a BUY Order (POST /api/v1/orders)

Splits $1000 into AAPL and TSLA (with an overridden TSLA price).

Request:

curl -X POST http://localhost:3000/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acc_123",
    "type": "BUY",
    "amount": 1000,
    "portfolio": [
      { "symbol": "AAPL", "allocation": 60 },
      { "symbol": "TSLA", "allocation": 40, "price": 250.5 }
    ]
  }'

Response (Success 201):

{
  "id": "abc-123-uuid",
  "accountId": "acc_123",
  "type": "BUY",
  "status": "EXECUTED",
  "totalAmount": 1000,
  "unallocatedCash": 0.21,
  "createdAt": "2026-07-07T14:00:00.000Z",
  "executionWindow": {
    "executeAt": "2026-07-07T14:00:00.000Z",
    "marketDay": "Tuesday",
    "note": "Market is open.",
    "isMarketOpen": true
  },
  "orders": [
    {
      "symbol": "AAPL",
      "allocation": 60,
      "targetAmount": 600,
      "executableAmount": 600,
      "price": 100,
      "quantity": 6,
      "type": "BUY"
    },
    {
      "symbol": "TSLA",
      "allocation": 40,
      "targetAmount": 400,
      "executableAmount": 399.79,
      "price": 250.5,
      "quantity": 1.596,
      "type": "BUY"
    }
  ]
}

Response (Validation Error 400): If allocations do not sum exactly to 100%.

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": [
    "Portfolio allocations must sum to exactly 100%"
  ],
  "timestamp": "2026-07-07T14:05:00.000Z",
  "path": "/api/v1/orders"
}

2. List Historic Orders (GET /api/v1/orders)

Request:

curl -X GET "http://localhost:3000/api/v1/orders?type=BUY&limit=10"

Response (Success 200):

{
  "data": [
    {
      "id": "abc-123-uuid",
      "accountId": "acc_123",
      "type": "BUY",
      "status": "EXECUTED",
      "totalAmount": 1000,
      "unallocatedCash": 0.21
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 1,
    "totalPages": 1
  }
}

(Note: The order object inside data is abbreviated here for brevity; it has the exact same shape as the complete create-order response above).

3. Get Order by ID (GET /api/v1/orders/:id)

Request:

curl -X GET http://localhost:3000/api/v1/orders/abc-123-uuid

Response (Success 200):

{
  "id": "abc-123-uuid",
  "accountId": "acc_123",
  "type": "BUY",
  "status": "EXECUTED",
  "totalAmount": 1000,
  "unallocatedCash": 0.21,
  "createdAt": "2026-07-07T14:00:00.000Z",
  "executionWindow": {
    "executeAt": "2026-07-07T14:00:00.000Z",
    "marketDay": "Tuesday",
    "note": "Market is open.",
    "isMarketOpen": true
  },
  "orders": [
    {
      "symbol": "AAPL",
      "allocation": 60,
      "targetAmount": 600,
      "executableAmount": 600,
      "price": 100,
      "quantity": 6,
      "type": "BUY"
    },
    {
      "symbol": "TSLA",
      "allocation": 40,
      "targetAmount": 400,
      "executableAmount": 399.79,
      "price": 250.5,
      "quantity": 1.596,
      "type": "BUY"
    }
  ]
}

About

A highly scalable backend service for a modern wealth management platform. This service receives a total investment amount and a model portfolio, and splits the total amount into individual stock orders based on target allocations.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages