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
- Ingest posts a placeholder. The
brokeragetransaction gets one debit against2010 Customer Wallet(customer paid) and one placeholder credit against4000 Brokerage Revenue. Same fortaf_sec_fee. - 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_usdDR to5000 Brokerage Costand CR to2340 ViewTrade Payable. - Posts the residual DR to
5099 Valura Earnings Shareand CR to2330 Valura Payablevia_apply_valura_accrual.
- Deletes the placeholder credit legs (via
- Idempotency.
recosted_aton theTransactionrow is set tonow(); the recost job filtersWHERE 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)
| Rate | Config | Meaning |
|---|---|---|
| 22 bps | VIEWTRADE_BROKERAGE_CHARGE_BPS | What Valura charges the customer |
| 4 bps | VIEWTRADE_BROKERAGE_COST_BPS | What 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_feefrom the Daily Ledger's TRD row, joined onorder_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) callrun_recost_batch()after each customer ifrecost=truein the request body (default). - Manually —
POST /v1/jobs/recost?limit=Nruns one batch.
What can go wrong
- Missing
broker_brokerageon the transaction. The recost falls back tobroker_cost_usd = fee_usd, meaning the whole customer charge goes to2340and the residual is $0 — pass-through fallback. Fix by ensuringviewtrade_importerpopulatesbroker_brokerage. - Recost on the wrong CoA. Running the UAE recost against a
ledger_indDB would try to post to Aldar codes absent from the India CoA. Guarded by source dispatch. - Double-recost.
recosted_at IS NULLfilter prevents this. To re-cost, set the column back to NULL manually and re-run.