Skip to content

42 - Action Center

Overview

The Action Center is a centralized order approval system for semi-automated trading. When enabled, orders are queued for manual approval before execution, essential for managed accounts and regulatory compliance (RA - Relationship Advisor mode).

Architecture Diagram

┌──────────────────────────────────────────────────────────────────────────────┐
│                        Action Center Architecture                            │
└──────────────────────────────────────────────────────────────────────────────┘

                           External Order Request
                           (TradingView, API, etc.)


┌──────────────────────────────────────────────────────────────────────────────┐
│                             Order Router Service                             │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │  should_route_to_pending(api_key, api_type)                          │    │
│  │                                                                      │    │
│  │  Check 1: Is user in semi_auto mode?                                │     │
│  │  Check 2: Is this a restricted operation?                           │     │
│  └─────────────────────────────────────────────────────────────────────┘     │
│                                    │                                         │
│              ┌─────────────────────┴─────────────────────┐                   │
│              │                                           │                   │
│          Auto Mode                                   Semi-Auto Mode          │
│          or Restricted                               (Queue Order)           │
│              │                                           │                   │
│              ▼                                           ▼                   │
│      Execute Immediately                        Create Pending Order         │
│      with Broker                                in Action Center             │
└──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────┐
│                               Action Center UI                               │
│                                /action-center                                │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │  [Pending (3)]  [Approved]  [Rejected]  [All Orders]                │     │
│  └─────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │  Statistics                                                          │    │
│  │  Pending: 3  │  Buy: 2  │  Sell: 1  │  Approved: 15  │  Rejected: 2 │     │
│  └─────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │  Strategy │ Symbol │ Exchange │ Action │ Qty │ Price │ Actions      │     │
│  ├─────────────────────────────────────────────────────────────────────┤     │
│  │  MyStrat  │ SBIN   │ NSE      │ BUY    │ 100 │ MKT   │ Approve      │     │
│  │           │        │          │        │     │       │ Reject       │     │
│  └─────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
│                            [Approve All Pending]                             │
└──────────────────────────────────────────────────────────────────────────────┘

                          User clicks Approve


┌──────────────────────────────────────────────────────────────────────────────┐
│                       Pending Order Execution Service                        │
│                                                                              │
│  1. Mark order status = 'approved'                                           │
│  2. Execute order with broker API                                            │
│  3. Get broker order status                                                  │
│  4. Update broker_order_id and broker_status                                 │
│  5. Emit SocketIO event                                                      │
└──────────────────────────────────────────────────────────────────────────────┘

Order Mode Configuration

Setting Order Mode

python
# Via API Key settings page
order_mode = 'auto'       # Direct execution (default)
order_mode = 'semi_auto'  # Queue for approval

Mode Toggle API

POST /apikey/mode
Content-Type: application/json

{"user_id": "admin", "mode": "semi_auto"}

Both user_id and mode are required. mode must be exactly auto or semi_auto; anything else returns HTTP 400.

Semi-Auto Workflow

┌─────────────────────────────────────────────────────────────────────────────┐
│                            Semi-Auto Order Flow                             │
│                                                                             │
│  1. Order Received ────────────────────────────────────────────────────►    │
│           │                                                                 │
│           ▼                                                                 │
│  2. Check Order Mode ──────────────────────────────────────────────────►    │
│           │                                                                 │
│           │ semi_auto = True                                                │
│           ▼                                                                 │
│  3. Create Pending Order ──────────────────────────────────────────────►    │
│           │                                                                 │
│           ├──► Store in pending_orders table                                │
│           │                                                                 │
│           ├──► Emit 'pending_order_created' SocketIO event                  │
│           │                                                                 │
│           └──► Return pending_order_id to caller                            │
│                       │                                                     │
│                       ▼                                                     │
│  4. User Reviews in Action Center ─────────────────────────────────────►    │
│           │                                                                 │
│           ├──────────────────┬──────────────────┐                           │
│           │                  │                  │                           │
│                Approve            Reject             Ignore                 │
│           │                  │                  │                           │
│           ▼                  ▼                  ▼                           │
│  5a. Execute Order    5b. Mark Rejected    5c. Stays Pending                │
│      with Broker          Store reason                                      │
│           │                  │                                              │
│           ▼                  ▼                                              │
│  6. Update Broker      Emit SocketIO                                        │
│     Status                Event                                             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Database Schema

pending_orders Table

┌─────────────────────────────────────────────────────────────────┐
│                      pending_orders table                       │
├─────────────────┬──────────────┬────────────────────────────────┤
│ Column          │ Type         │ Description                    │
├─────────────────┼──────────────┼────────────────────────────────┤
│ id              │ INTEGER PK   │ Unique order identifier        │
│ user_id         │ VARCHAR(255) │ User who placed order          │
│ api_type        │ VARCHAR(50)  │ Order type                     │
│ order_data      │ TEXT         │ JSON order details             │
│ created_at      │ DATETIME     │ Creation time (UTC)            │
│ created_at_ist  │ VARCHAR(50)  │ Creation time (IST)            │
│ status          │ VARCHAR(20)  │ pending/approved/rejected      │
│ approved_at     │ DATETIME     │ Approval time (UTC)            │
│ approved_at_ist │ VARCHAR(50)  │ Approval time (IST)            │
│ approved_by     │ VARCHAR(255) │ Approver username              │
│ rejected_at     │ DATETIME     │ Rejection time (UTC)           │
│ rejected_at_ist │ VARCHAR(50)  │ Rejection time (IST)           │
│ rejected_by     │ VARCHAR(255) │ Rejector username              │
│ rejected_reason │ TEXT         │ Reason for rejection           │
│ broker_order_id │ VARCHAR(255) │ Broker's order ID              │
│ broker_status   │ VARCHAR(20)  │ complete/open/rejected         │
└─────────────────┴──────────────┴────────────────────────────────┘

Indexes

sql
CREATE INDEX idx_user_status ON pending_orders(user_id, status);
CREATE INDEX idx_created_at ON pending_orders(created_at);

Executor-Supported Queued Types

API TypeDescriptionService called on approval
placeorderStandard orderplace_order_service.place_order
smartorderPosition-aware orderplace_smart_order_service.place_smart_order
basketorderMultiple ordersbasket_order_service.place_basket_order
splitorderSplit large orderssplit_order_service.split_order
optionsorderOptions contractsplace_options_order_service.place_options_order
optionsmultiorderMulti-leg options orderoptions_multiorder_service.place_options_multiorder
placegttorderGTT order placementplace_gtt_order_service.place_gtt_order

Approval dispatch in services/pending_order_execution_service.py handles these seven API types. Any other api_type is answered with Unknown order type: <api_type> and HTTP 400, and the row's broker status is set to rejected.

placegttorder is handled specially on success: the executor stores the broker's trigger_id as broker_order_id and sets broker_status to open. If the GTT placement succeeds without a trigger ID, the row is marked rejected and the call returns HTTP 502.

calculate_action_center_stats() reports a counter per API type as well: total_placeorder, total_smartorder, total_basketorder, total_splitorder, total_optionsorder, total_optionsmultiorder and total_placegttorder.

Restricted Operations

These operations are never queued. Read-only operations execute immediately; destructive operations apply their own service policy and are blocked in semi-auto mode unless analyzer behavior explicitly permits them. The set is IMMEDIATE_EXECUTION_OPERATIONS in services/order_router_service.py, and should_route_to_pending() compares against it case-insensitively.

OperationReason
closepositionBlocked by close-position service in semi-auto live mode
closeallpositionsBlocked by close-position service in semi-auto live mode
cancelorderBlocked by cancel-order service in semi-auto live mode
cancelallorderBlocked by cancel-all service in semi-auto live mode
modifyorderBlocked by modify-order service in semi-auto live mode
modifygttorder / cancelgttorderBlocked because a delayed change may target a stale trigger
gttorderbookData retrieval
orderstatusStatus query
orderbookData retrieval
tradebookData retrieval
positions / openpositionData retrieval
holdingsData retrieval
fundsData retrieval

API Endpoints

All Action Center routes are defined on orders_bp (blueprints/orders.py, url_prefix="/") and require a valid session. Every route except /action-center/count also carries @limiter.limit(API_RATE_LIMIT).

EndpointMethodDescription
/action-centerGETServer-rendered page (the React SPA serves the same path)
/action-center/api/dataGETOrders and statistics as JSON
/action-center/approve/<order_id>POSTApprove and execute one order
/action-center/reject/<order_id>POSTReject one order
/action-center/delete/<order_id>DELETEDelete one non-pending order
/action-center/approve-allPOSTApprove and execute every pending order
/action-center/countGETPending count for the nav badge

Get Orders

GET /action-center/api/data?status=pending

status accepts pending, approved, rejected or all, and defaults to pending. Statistics are always calculated over all of the user's orders, not just the filtered subset.

Response:

json
{
    "status": "success",
    "data": {
        "orders": [
            {
                "id": 1,
                "user_id": "admin",
                "api_type": "placeorder",
                "status": "pending",
                "created_at_ist": "2024-01-15 09:30:00 IST",
                "strategy": "MyStrategy",
                "symbol": "SBIN",
                "exchange": "NSE",
                "action": "BUY",
                "quantity": 100,
                "price": "0",
                "trigger_price": "0",
                "price_type": "MARKET",
                "product_type": "MIS",
                "broker_order_id": null,
                "broker_status": null,
                "raw_order_data": {}
            }
        ],
        "statistics": {
            "total_pending": 3,
            "total_approved": 15,
            "total_rejected": 2,
            "total_buy_orders": 10,
            "total_sell_orders": 10,
            "total_placeorder": 20,
            "total_smartorder": 0,
            "total_basketorder": 0,
            "total_splitorder": 0,
            "total_optionsorder": 0,
            "total_optionsmultiorder": 0,
            "total_placegttorder": 0
        }
    }
}

raw_order_data is the stored payload with apikey and api_key stripped out.

Approve Order

POST /action-center/approve/{order_id}

Response:

json
{
    "status": "success",
    "message": "Order approved and executed successfully",
    "broker_order_id": "123456789"
}

If the order is approved but broker execution fails, the response is {"status": "warning", "message": "Order approved but execution failed", "error": "..."} returned with the service's own status code.

Reject Order

POST /action-center/reject/{order_id}
Content-Type: application/json

{"reason": "Invalid price level"}

Approve All

POST /action-center/approve-all

Response:

json
{
    "status": "success",
    "message": "Successfully approved and executed all 5 orders",
    "approved_count": 5,
    "executed_count": 5,
    "failed_executions": []
}

status is success when every approved order executed, warning when some executed, and error when none did. With nothing pending, the response is {"status": "info", "message": "No pending orders to approve"}.

Delete Order

DELETE /action-center/delete/{order_id}

Note: Only approved or rejected orders can be deleted.

Get Pending Count

GET /action-center/count

Response:

json
{
    "count": 3
}

Real-Time Updates

SocketIO Events

EventTriggerData
pending_order_createdNew order queued by order_router_service.queue_order()pending_order_id, user_id, api_type, message
pending_order_updatedApprove, reject, delete or approve-allaction (approved, rejected, deleted or batch_approved), order_id, user_id. The batch variant sends count instead of order_id

pending_order_created is emitted through socketio.start_background_task() and is best-effort: if the emit fails, the queued row still stands and the caller still receives a success response.

Frontend Handling

typescript
// Listen for new orders
socket.on('pending_order_created', () => {
    playAlertSound();
    showToast('New order pending approval');
    refreshOrders();
});

// Listen for status changes
socket.on('pending_order_updated', () => {
    refreshOrders();
});

React Component Features

Tabbed Interface

[Pending (3)]  [Approved]  [Rejected]  [All Orders]

  (pulse animation when pending > 0)

Statistics Dashboard

┌─────────────────────────────────────────────────────────────────┐
│  Pending: 3    │    Buy: 2    │    Sell: 1    │    Approved: 15 │
│  (yellow)           (green)        (red)            (green)     │
└─────────────────────────────────────────────────────────────────┘

Order Table Columns

ColumnContent
StrategyStrategy name
SymbolTrading symbol
ExchangeNSE/NFO/MCX badge
ActionBUY (green) / SELL (red)
QuantityOrder quantity
PricePrice or "MARKET"
Order Typeplaceorder/smartorder/etc
ProductCNC/MIS/NRML badge
CreatedRelative time ("5 min ago")
ActionsApprove/Reject/Delete buttons

Expandable Details

Click chevron to view raw order data:

┌─────────────────────────────────────────────────────────────────┐
│  ▼ Order Details                                                │
│                                                                 │
│  apikey: ****                                                   │
│  strategy: MyStrategy                                           │
│  symbol: SBIN                                                   │
│  exchange: NSE                                                  │
│  action: BUY                                                    │
│  quantity: 100                                                  │
│  pricetype: MARKET                                              │
│  product: MIS                                                   │
└─────────────────────────────────────────────────────────────────┘

Service Implementation

Order Router

python
def should_route_to_pending(api_key, api_type=None):
    """Return True only for queue-eligible requests in semi-auto mode."""
    if api_type and api_type.lower() in IMMEDIATE_EXECUTION_OPERATIONS:
        return False
    user_id = verify_api_key(api_key)
    if not user_id:
        return False
    return get_order_mode(user_id) == 'semi_auto'

Any exception inside should_route_to_pending() is caught and returns False, so an unexpected failure falls back to immediate execution rather than silently queueing.

Queue Order

python
def queue_order(api_key, order_data, api_type):
    """Queue order for approval"""
    user_id = verify_api_key(api_key)
    if not user_id:
        return False, {'status': 'error', 'message': 'Invalid API key'}, 403

    # The API key is stripped before the payload is persisted
    order_data_clean = order_data.copy()
    order_data_clean.pop('apikey', None)

    pending_order_id = create_pending_order(user_id, api_type, order_data_clean)

    # Emit best-effort UI notification after persistence.
    socketio.start_background_task(
        socketio.emit,
        'pending_order_created',
        {
            'pending_order_id': pending_order_id,
            'user_id': user_id,
            'api_type': api_type,
            'message': f'New {api_type} order queued for approval',
        },
    )

    return True, {
        'status': 'success',
        'message': 'Order queued for approval in Action Center',
        'mode': 'semi_auto',
        'pending_order_id': pending_order_id
    }, 200

Execute Approved Order

python
def execute_approved_order(pending_order_id) -> tuple[bool, dict, int]:
    """Execute approved order with broker"""
    pending_order = get_pending_order_by_id(pending_order_id)
    if not pending_order:
        return False, {'status': 'error', 'message': 'Pending order not found'}, 404

    # Only a row already marked 'approved' can be executed
    if pending_order.status != 'approved':
        return False, {'status': 'error', 'message': '...'}, 400

    order_data = json.loads(pending_order.order_data)

    # Credentials are resolved at execution time, never stored on the row
    api_key = get_api_key_for_tradingview(pending_order.user_id)
    auth_token = get_auth_token(pending_order.user_id)
    broker = Auth.query.filter_by(name=pending_order.user_id).first().broker

    # Route to appropriate service on pending_order.api_type
    if pending_order.api_type == 'placeorder':
        success, response_data, status_code = place_order(
            order_data=order_data, api_key=api_key, auth_token=auth_token, broker=broker
        )
    # ... six other types

    update_broker_status(pending_order_id, broker_order_id, broker_status)
    return success, response_data, status_code

Passing auth_token and broker into the service is what stops the approved order from being routed back into the pending queue a second time. If any of api_key, auth_token or broker is missing, the row is marked rejected and the call returns HTTP 403.

Batch responses (basket and split orders) are flattened by _flatten_execution_results() and the resulting broker order IDs are joined into the 255 character broker_order_id column, truncated to <first_id> (+N more) when they do not fit.

Security & Compliance

Audit Trail

All actions are logged with:

  • Timestamp (IST)
  • Username
  • Action taken
  • Reason (for rejections)

API Key Security

  • API keys never stored in pending_orders
  • Only user_id reference maintained
  • Keys retrieved at execution time

Analyzer Mode Restriction

When in semi_auto mode, the API-key path of the analyzer toggle (services/analyzer_service.py) is blocked with HTTP 403 to ensure SEBI Research Analyst compliance: mode switching stays a client-only decision made through the UI. Internal calls that already carry auth_token and broker are not affected.

Key Files Reference

FilePurpose
database/action_center_db.pyPendingOrder model
services/action_center_service.pyOrder parsing, stats
services/order_router_service.pyRoute decisions
services/pending_order_execution_service.pyExecute approved
blueprints/orders.pyAction center routes
blueprints/apikey.pyMode toggle
frontend/src/pages/ActionCenter.tsxReact UI