Skip to main content

Recost pipeline

Some brokers (GTN, ViewTrade) BUNDLE brokerage + fees into the executed trade price rather than itemizing them. Ingest can't split them at post-time because the transaction record doesn't carry the split. So the ingest posts a placeholder and a follow-up recost job replaces the placeholder legs with the real broker/Valura split.

The pattern

  1. Ingest posts a placeholder. The brokerage transaction gets one debit against 2010 Customer Wallet (customer paid) and one placeholder credit against 4000 Brokerage Revenue. Same for taf_sec_fee.
  2. Recost overwrites the placeholders. apply_recost_viewtrade(txn_id, fee_usd, customer_id, txn_type, broker_cost_usd):
    • Deletes the placeholder credit legs (via _delete_placeholder_legs).
    • Posts broker_cost_usd DR to 5000 Brokerage Cost and CR to 2340 ViewTrade Payable.
    • Posts the residual DR to 5099 Valura Earnings Share and CR to 2330 Valura Payable via _apply_valura_accrual.
  3. Idempotency. recosted_at on the Transaction row is set to now(); the recost job filters WHERE recosted_at IS NULL.

The two dispatchers

services/recost_job.py iterates pending transactions and dispatches by source:

if txn.source == "VIEWTRADE":
await apply_recost_viewtrade(session, txn.txn_id, fee_usd=txn.amount_usd,
customer_id=txn.customer_id, txn_type=txn.txn_type,
broker_cost_usd=txn.broker_brokerage)
elif txn.source in ("GTN", "ZAG"):
await apply_recost_gtn(session, ...)

The 22 / 4 / 18 bps split (India)

RateConfigMeaning
22 bpsVIEWTRADE_BROKERAGE_CHARGE_BPSWhat Valura charges the customer
4 bpsVIEWTRADE_BROKERAGE_COST_BPSWhat ViewTrade actually costs Valura
18 bps(derived)Valura's residual margin

For a $1000 notional trade:

  • Customer wallet debit: $1000 principal + $2.20 brokerage = $1002.20
  • Custody debit: $1000
  • ViewTrade Payable credit: $0.40 (4 bps of $1000)
  • Valura Payable credit: $1.80 (18 bps residual)

D2 — actual vs modeled

The M5 D2 broker-fee reconciliation (services/india_broker_fees.py) joins the ViewTrade Daily Ledger CSV (via services/viewtrade_report_importer.py) to get ACTUAL itemized broker fees per trade. Per (customer, symbol, date):

  • modeled = what recost posted to 2340
  • expected = VIEWTRADE_BROKERAGE_COST_BPS × notional
  • actual = commission + sec_fee + brk_fee + ifsca_fee + oth_fee + tefra_fee from the Daily Ledger's TRD row, joined on order_ref

Drift against actual is the real signal. In practice the flat-bps assumption doesn't survive small-ticket fractional trades where ViewTrade's fixed per-trade fees dominate.

When to run it

  • Automatically — the ingest jobs (sync-viewtrade, sync-viewtrade-trades) call run_recost_batch() after each customer if recost=true in the request body (default).
  • ManuallyPOST /v1/jobs/recost?limit=N runs one batch.

What can go wrong

  • Missing broker_brokerage on the transaction. The recost falls back to broker_cost_usd = fee_usd, meaning the whole customer charge goes to 2340 and the residual is $0 — pass-through fallback. Fix by ensuring viewtrade_importer populates broker_brokerage.
  • Recost on the wrong CoA. Running the UAE recost against a ledger_ind DB would try to post to Aldar codes absent from the India CoA. Guarded by source dispatch.
  • Double-recost. recosted_at IS NULL filter prevents this. To re-cost, set the column back to NULL manually and re-run.