Skip to content

19 - PlaceOrder Call Flow

Overview

The PlaceOrder API is the core order execution endpoint in Tradeboard. It handles order validation, authentication, broker routing, and response processing through multiple layers.

Complete Flow Diagram

┌──────────────────────────────────────────────────────────────────────────────┐
│                           PlaceOrder Complete Flow                           │
└──────────────────────────────────────────────────────────────────────────────┘

                             Client Request (JSON)


┌──────────────────────────────────────────────────────────────────────────────┐
│  Layer 1: REST resource   restx_api/place_order.py                           │
│  POST /api/v1/placeorder  ->  class PlaceOrder(Resource).post                │
│                                                                              │
│  @limiter.limit(ORDER_RATE_LIMIT)      10 per second by default              │
│  track_latency("PLACE")                wrapped onto the Resource at          │
│                                        startup by utils/latency_monitor.py   │
│                                                                              │
│  order_data = OrderSchema().load(request.json)                               │
│  ValidationError  ->  400 without reaching the service layer                 │
│  api_key = order_data.get("apikey")                                          │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│  Layer 2: services/place_order_service.py   place_order(...)                 │
│                                                                              │
│  Step 1  Action Center routing                                               │
│          should_route_to_pending(api_key, "placeorder")                      │
│          semi-auto  ->  queue_order(...) and return here                     │
│          auto       ->  continue                                             │
│                                                                              │
│  Step 2  validate_order_data(order_data)                                     │
│          REQUIRED_ORDER_FIELDS, VALID_EXCHANGES, VALID_ACTIONS,              │
│          VALID_PRICE_TYPES, VALID_PRODUCT_TYPES, then OrderSchema again      │
│          failure  ->  OrderFailedEvent, or AnalyzerErrorEvent in             │
│                       analyzer mode  ->  400                                 │
│                                                                              │
│  Step 3  get_auth_token_broker(api_key)      database/auth_db.py             │
│          None  ->  403 "Invalid tradeboard apikey", deliberately not           │
│                    logged so a key-guessing loop cannot flood the DB         │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│  Layer 3: place_order_with_auth(...)   mode routing                          │
│                                                                              │
│  if get_analyze_mode():                database/settings_db.py               │
│      services/sandbox_service.py  ->  sandbox_place_order(...)               │
│      sandbox/order_manager.py     ->  OrderManager(user_id).place_order()    │
│      bus.publish(OrderPlacedEvent(mode="analyze", ...)) and return           │
│  else:                                                                       │
│      import_broker_module(broker_name)                                       │
│      importlib.import_module(f"broker.{broker_name}.api.order_api")          │
│      import failure  ->  OrderFailedEvent  ->  404                           │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│  Layer 4: broker plugin                                                      │
│  broker_module.place_order_api(order_data, auth_token)                       │
│                                                                              │
│  A  transform_data(data)     broker/<key>/mapping/transform_data.py          │
│     Tradeboard field names  ->  broker field names                             │
│                                                                              │
│  B  get_br_symbol(symbol, exchange)      database/token_db.py                │
│     "SBIN" + "NSE"  ->  "SBIN-EQ" for Zerodha                                │
│                                                                              │
│  C  get_httpx_client().post(<broker order URL>, ...)                         │
│     shared pooled HTTP client, broker auth header                            │
│                                                                              │
│  D  return (response, response_data, order_id)                               │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│  Layer 5: result handling and fan-out                                        │
│                                                                              │
│  res.status == 200                   res.status != 200, or exception         │
│  bus.publish(                        bus.publish(                            │
│      OrderPlacedEvent(...))              OrderFailedEvent(...))              │
│                                                                              │
│  subscribers/__init__.py fans "order.placed" and "order.failed" out to       │
│  log, socketio, telegram and whatsapp subscribers on the EventBus pool.      │
│  log_subscriber writes order_logs (live) or analyzer_logs (analyze).         │
│                                                                              │
│  restx_api/place_order.py returns                                            │
│  make_response(jsonify(response_data), status_code)                          │
│  track_latency then writes order_latency off-thread.                         │
└──────────────────────────────────────────────────────────────────────────────┘

Request Format

Basic Order Request

json
{
    "apikey": "your_api_key",
    "strategy": "MyStrategy",
    "symbol": "SBIN",
    "exchange": "NSE",
    "action": "BUY",
    "quantity": 100,
    "product": "MIS",
    "pricetype": "MARKET"
}

Limit Order

json
{
    "apikey": "your_api_key",
    "strategy": "MyStrategy",
    "symbol": "INFY",
    "exchange": "NSE",
    "action": "BUY",
    "quantity": 50,
    "product": "CNC",
    "pricetype": "LIMIT",
    "price": 1650.00
}

Stop-Loss Order

json
{
    "apikey": "your_api_key",
    "strategy": "MyStrategy",
    "symbol": "NIFTY21JAN2521500CE",
    "exchange": "NFO",
    "action": "BUY",
    "quantity": 65,
    "product": "MIS",
    "pricetype": "SL",
    "price": 250.00,
    "trigger_price": 245.00
}

Validation Rules

Mandatory Fields

FieldTypeDescription
apikeystringTradeboard API key
strategystringStrategy identifier
symbolstringTrading symbol
exchangestringExchange code
actionstringBUY or SELL
quantityintegerOrder quantity (≥1)

Valid Values

utils/constants.py is the single source of truth:

VALID_EXCHANGES:    NSE, NFO, CDS, BSE, BFO, BCD, MCX, NCDEX, NCO,
                    NSE_INDEX, BSE_INDEX, MCX_INDEX, GLOBAL_INDEX, CRYPTO

VALID_ACTIONS:      BUY, SELL
                    OrderSchema also accepts lowercase buy and sell

VALID_PRICE_TYPES:  MARKET, LIMIT, SL, SL-M

VALID_PRODUCT_TYPES: CNC (delivery), MIS (intraday), NRML (F&O carryforward)

Defaults when the field is omitted come from OrderSchema and utils/constants.py: pricetype defaults to MARKET, product to MIS, and price, trigger_price and disclosed_quantity to "0". quantity arrives as a float and is coerced to an integer by the schema's coerce_quantity post-load hook.

Order Routing Modes

Auto Mode (Default)

Request → Validate → Authenticate → Execute → Response

Orders are executed immediately without manual intervention.

Semi-Auto Mode

Request → Validate → Queue to Action Center → Await Approval

                                              ┌─────┴─────┐
                                              │           │
                                          Approved    Rejected
                                              │           │
                                              ▼           ▼
                                          Execute     Discard

Orders require manual approval before execution.

Analyzer Mode (Sandbox)

When analyze_mode = True:

┌──────────────────────────────────────────────────────────────────┐
│                        Sandbox Execution                         │
│                                                                  │
│  1. Initialize OrderManager(user_id)                             │
│  2. Check sandbox funds (₹1 Crore default)                       │
│  3. Calculate margin requirements                                │
│  4. Simulate order execution                                     │
│  5. Update sandbox positions                                     │
│  6. Log to analyzer_db                                           │
│  7. Return same response format as live                          │
└──────────────────────────────────────────────────────────────────┘

Broker Integration

Dynamic Module Loading

python
def import_broker_module(broker_name):
    module_path = f'broker.{broker_name}.api.order_api'
    return importlib.import_module(module_path)

Broker-Specific Implementation

Each broker implements:

python
def place_order_api(data, auth):
    # 1. Transform data to broker format
    transformed = transform_data(data)

    # 2. Map symbol to broker format
    symbol = get_br_symbol(data['symbol'], data['exchange'])

    # 3. Make HTTP request to broker API
    response = client.post(BROKER_ORDER_URL, data=transformed)

    # 4. Parse response
    order_id = response.json()['data']['order_id']

    return (response, response_data, order_id)

Error Handling

ErrorHTTP CodeRaised in
Marshmallow ValidationError400restx_api/place_order.py, before the service is called
Missing field, invalid exchange, action, pricetype or product400validate_order_data() in the service
Sandbox call without apikey, or neither api_key nor auth_token plus broker400place_order() / place_order_with_auth()
Invalid or revoked API key403get_auth_token_broker() returned None; the message is Invalid tradeboard apikey
Broker module import failed404import_broker_module(); the message is Broker-specific module not found
Broker rejected the orderbroker statusplace_order_with_auth() forwards res.status, falling back to 500
Unhandled exception500place_order_with_auth() or the resource's outer except
Rate limit429Flask-Limiter, handled by the @app.errorhandler(429) in app.py

The resource always returns make_response(jsonify(response_data), status_code), so the status code is whatever the service returned. Nothing in the resource rewrites a broker status into a generic 500.

Event Bus Side-Effects

All post-order side-effects (logging, Socket.IO, Telegram, WhatsApp) are dispatched through the Event Bus. The service publishes a single typed event; subscribers handle each concern independently.

Publishing

python
from events import OrderPlacedEvent
from utils.event_bus import bus

# After a successful broker call, one publish replaces three call sites
bus.publish(OrderPlacedEvent(
    mode="live",
    api_type="placeorder",
    symbol=order_data["symbol"],
    action=order_data["action"],
    orderid=str(order_id),
    request_data=cleaned_request,
    response_data={"status": "success", "orderid": order_id},
    api_key=api_key,
))

What Subscribers Do

subscribers/__init__.py subscribes four modules to both order.placed and order.failed:

SubscriberAction
log_subscriber_log_event routes on event.mode: analyze writes analyzer_logs via async_log_analyzer, anything else writes order_logs via async_log_order
socketio_subscriberEmits the live order event or the analyzer update to the browser
telegram_subscriberSends the Telegram alert
whatsapp_subscriberSends the WhatsApp alert

bus.publish() never blocks the request thread. EventBus dispatches on a shared ThreadPoolExecutor(max_workers=10, thread_name_prefix="eventbus") with a bounded pending queue of 1000; over-capacity events are dropped with a warning rather than queued indefinitely.

See 53-event-bus for full architecture details.

Security Layers

API Key Verification

┌─────────────────────────────────────────┐
│ 1. Add pepper to provided API key       │
│    peppered = api_key + API_KEY_PEPPER  │
├─────────────────────────────────────────┤
│ 2. Check invalid cache (5-min TTL)      │
│    Fast rejection of bad keys           │
├─────────────────────────────────────────┤
│ 3. Check verified cache (10-hour TTL)   │
│    Fast path for good keys              │
├─────────────────────────────────────────┤
│ 4. Argon2 hash comparison               │
│    Full verification if cache miss      │
├─────────────────────────────────────────┤
│ 5. Decrypt auth token with Fernet       │
│    AES-128 CBC encryption               │
└─────────────────────────────────────────┘

Request Sanitization

  • API keys removed from logs
  • Sensitive data encrypted at rest
  • Rate limiting per endpoint

Performance Optimizations

OptimizationDescription
Connection poolingHTTP clients reuse connections
API key cachingReduce Argon2 hashing overhead
Async loggingNon-blocking order logs
Thread pool10 worker threads for async ops

Key Files Reference

FilePurpose
restx_api/place_order.pyREST resource, rate limit, response shaping
restx_api/schemas.pyOrderSchema request validation
services/place_order_service.pyplace_order, place_order_with_auth, validate_order_data, import_broker_module
services/order_router_service.pySemi-auto routing (should_route_to_pending, queue_order)
services/sandbox_service.pyAnalyzer-mode entry (sandbox_place_order)
sandbox/order_manager.pyOrderManager.place_order simulated execution
database/settings_db.pyget_analyze_mode() mode flag
database/auth_db.pyget_auth_token_broker() and the API key caches
broker/{name}/api/order_api.pyplace_order_api(data, auth)
broker/{name}/mapping/transform_data.pyData transformation
utils/event_bus.pyEventBus and the module-level bus singleton
events/order_events.pyOrderPlacedEvent (order.placed), OrderFailedEvent (order.failed)
subscribers/Log, Socket.IO, Telegram and WhatsApp subscribers
database/apilog_db.pyorder_logs writes from log_subscriber
utils/latency_monitor.pytrack_latency("PLACE") wrapper and order_latency writes