296 lines
11 KiB
Rust
296 lines
11 KiB
Rust
//! Commission, tax, and fee model for Indian and global markets.
|
||||
|
|
//!
|
|||
|
|
//! All `_rate` fields are fractions (0.001 = 0.1%).
|
|||
|
|
//! All per-unit fields (`flat_per_order`, `per_lot`) are in base currency units (e.g., INR).
|
|||
|
|
//! The model is self-contained: pass `trade_value`, `num_lots`, `is_buy` to get total cost.
|
|||
|
|
|
|||
|
|
#[cfg(feature = "serde")]
|
|||
|
|
use serde::{Deserialize, Serialize};
|
|||
|
|
|
|||
|
|
/// Advanced commission and tax model.
|
|||
|
|
///
|
|||
|
|
/// # Fields (all public for direct construction)
|
|||
|
|
/// - **Brokerage**: `flat_per_order`, `rate_of_value`, `per_lot`, `max_brokerage`
|
|||
|
|
/// - **STT**: `stt_rate`, `stt_on_buy`, `stt_on_sell`
|
|||
|
|
/// - **Levies**: `exchange_charges_rate`, `regulatory_charges_rate`, `gst_rate`, `stamp_duty_rate`
|
|||
|
|
/// - **Sizing**: `lot_size`
|
|||
|
|
///
|
|||
|
|
/// # Indian market notes
|
|||
|
|
/// - STT (Securities Transaction Tax) is applied on turnover (buy/sell legs vary by segment).
|
|||
|
|
/// - Exchange charges and regulatory body charges are on turnover.
|
|||
|
|
/// - GST (18%) applies on brokerage + exchange charges + regulatory body charges (not STT/stamp).
|
|||
|
|
/// - Stamp duty is on buy-side value only.
|
|||
|
|
#[derive(Clone, Debug, PartialEq)]
|
|||
|
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|||
|
|
pub struct CommissionModel {
|
|||
|
|
// --- Brokerage ---------------------------------------------------------
|
|||
|
|
/// Fixed fee per order (e.g., ₹20 flat fee per order). 0.0 = none.
|
|||
|
|
pub flat_per_order: f64,
|
|||
|
|
/// Proportional brokerage as fraction of `trade_value` (e.g., 0.001 = 0.1%). 0.0 = none.
|
|||
|
|
pub rate_of_value: f64,
|
|||
|
|
/// Fixed fee per lot (e.g., ₹2 per lot). 0.0 = none.
|
|||
|
|
pub per_lot: f64,
|
|||
|
|
/// Brokerage cap in currency units. 0.0 = no cap.
|
|||
|
|
/// Effective brokerage = min(flat + rate × value + per_lot × lots, max_brokerage).
|
|||
|
|
pub max_brokerage: f64,
|
|||
|
|
/// Bid-ask spread model in basis points. Half-spread is paid on each leg (entry and exit),
|
|||
|
|
/// so total roundtrip cost = spread_bps in bps. 0.0 = no spread cost.
|
|||
|
|
pub spread_bps: f64,
|
|||
|
|
|
|||
|
|
// --- Securities Transaction Tax (STT) ----------------------------------
|
|||
|
|
/// STT rate as fraction of trade value. 0.0 = no STT.
|
|||
|
|
pub stt_rate: f64,
|
|||
|
|
/// Apply STT on the buy leg.
|
|||
|
|
pub stt_on_buy: bool,
|
|||
|
|
/// Apply STT on the sell leg.
|
|||
|
|
pub stt_on_sell: bool,
|
|||
|
|
|
|||
|
|
// --- Exchange & Regulatory Levies --------------------------------------
|
|||
|
|
/// Exchange transaction charges rate (fraction of trade value).
|
|||
|
|
pub exchange_charges_rate: f64,
|
|||
|
|
/// Regulatory body turnover charges rate (fraction of trade value). Typically ~0.000001.
|
|||
|
|
pub regulatory_charges_rate: f64,
|
|||
|
|
/// Indirect tax (GST) rate applied on (brokerage + exchange_charges + regulatory_charges).
|
|||
|
|
/// Typically 0.18 in India.
|
|||
|
|
pub gst_rate: f64,
|
|||
|
|
/// Stamp duty rate on buy side only (fraction of trade value).
|
|||
|
|
pub stamp_duty_rate: f64,
|
|||
|
|
|
|||
|
|
// --- Instrument Sizing ------------------------------------------------
|
|||
|
|
/// Lot size for the instrument.
|
|||
|
|
/// Equities: 1.0. Index futures/options: contract lot size (e.g., 25, 50, 75).
|
|||
|
|
/// Used for per_lot cost: cost += per_lot × ceil(quantity / lot_size).
|
|||
|
|
pub lot_size: f64,
|
|||
|
|
|
|||
|
|
// --- Short Selling ----------------------------------------------------
|
|||
|
|
/// Annualised short borrow rate as a fraction (e.g. 0.03 = 3% p.a.).
|
|||
|
|
/// Applied per bar to short positions. 0.0 = no borrow cost.
|
|||
|
|
pub short_borrow_rate_annual: f64,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Default for CommissionModel {
|
|||
|
|
fn default() -> Self {
|
|||
|
|
Self {
|
|||
|
|
flat_per_order: 0.0,
|
|||
|
|
rate_of_value: 0.0,
|
|||
|
|
per_lot: 0.0,
|
|||
|
|
max_brokerage: 0.0,
|
|||
|
|
spread_bps: 0.0,
|
|||
|
|
stt_rate: 0.0,
|
|||
|
|
stt_on_buy: false,
|
|||
|
|
stt_on_sell: false,
|
|||
|
|
exchange_charges_rate: 0.0,
|
|||
|
|
regulatory_charges_rate: 0.0,
|
|||
|
|
gst_rate: 0.0,
|
|||
|
|
stamp_duty_rate: 0.0,
|
|||
|
|
lot_size: 1.0,
|
|||
|
|
short_borrow_rate_annual: 0.0,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl CommissionModel {
|
|||
|
|
// ------------------------------------------------------------------
|
|||
|
|
// Core computation
|
|||
|
|
// ------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
/// Compute total transaction cost in **absolute currency units**.
|
|||
|
|
///
|
|||
|
|
/// # Parameters
|
|||
|
|
/// - `trade_value`: price × quantity in base currency
|
|||
|
|
/// - `num_lots`: number of lots transacted
|
|||
|
|
/// - `is_buy`: true for buy (entry) leg, false for sell (exit) leg
|
|||
|
|
pub fn total_cost(&self, trade_value: f64, num_lots: f64, is_buy: bool) -> f64 {
|
|||
|
|
// Brokerage (optionally capped)
|
|||
|
|
let raw_brokerage =
|
|||
|
|
self.flat_per_order + self.rate_of_value * trade_value + self.per_lot * num_lots;
|
|||
|
|
let brokerage = if self.max_brokerage > 0.0 {
|
|||
|
|
raw_brokerage.min(self.max_brokerage)
|
|||
|
|
} else {
|
|||
|
|
raw_brokerage
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// STT
|
|||
|
|
let stt = if (is_buy && self.stt_on_buy) || (!is_buy && self.stt_on_sell) {
|
|||
|
|
self.stt_rate * trade_value
|
|||
|
|
} else {
|
|||
|
|
0.0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let exchange = self.exchange_charges_rate * trade_value;
|
|||
|
|
let regulatory = self.regulatory_charges_rate * trade_value;
|
|||
|
|
|
|||
|
|
// GST on brokerage + exchange + regulatory (NOT on STT or stamp duty)
|
|||
|
|
let gst = self.gst_rate * (brokerage + exchange + regulatory);
|
|||
|
|
|
|||
|
|
// Stamp duty only on buy side
|
|||
|
|
let stamp = if is_buy {
|
|||
|
|
self.stamp_duty_rate * trade_value
|
|||
|
|
} else {
|
|||
|
|
0.0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Bid-ask spread: half-spread paid on each leg
|
|||
|
|
let spread_cost = self.spread_bps / 2.0 / 10_000.0 * trade_value;
|
|||
|
|
|
|||
|
|
brokerage + stt + exchange + regulatory + gst + stamp + spread_cost
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Borrow cost per bar for a short position.
|
|||
|
|
///
|
|||
|
|
/// # Parameters
|
|||
|
|
/// - `trade_value`: abs(price × quantity)
|
|||
|
|
/// - `periods_per_year`: 252 for daily, 52 for weekly, etc.
|
|||
|
|
pub fn short_borrow_cost(&self, trade_value: f64, periods_per_year: f64) -> f64 {
|
|||
|
|
if self.short_borrow_rate_annual <= 0.0 || periods_per_year <= 0.0 {
|
|||
|
|
return 0.0;
|
|||
|
|
}
|
|||
|
|
self.short_borrow_rate_annual / periods_per_year * trade_value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Compute cost as a **fraction of `initial_capital`** for use in normalised equity loops.
|
|||
|
|
///
|
|||
|
|
/// Returns 0.0 if `initial_capital` ≤ 0.
|
|||
|
|
pub fn cost_fraction(
|
|||
|
|
&self,
|
|||
|
|
trade_value: f64,
|
|||
|
|
num_lots: f64,
|
|||
|
|
is_buy: bool,
|
|||
|
|
initial_capital: f64,
|
|||
|
|
) -> f64 {
|
|||
|
|
if initial_capital <= 0.0 {
|
|||
|
|
return 0.0;
|
|||
|
|
}
|
|||
|
|
self.total_cost(trade_value, num_lots, is_buy) / initial_capital
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------
|
|||
|
|
// Built-in Presets
|
|||
|
|
// ------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
/// Zero commission — useful for clean research/comparison runs.
|
|||
|
|
pub fn zero() -> Self {
|
|||
|
|
Self::default()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Indian equity **delivery** (long-term hold).
|
|||
|
|
///
|
|||
|
|
/// Brokerage: 0.1% (capped at ₹20), STT 0.1% both sides,
|
|||
|
|
/// exchange charges, regulatory body charges, 18% GST, stamp duty.
|
|||
|
|
pub fn equity_delivery_india() -> Self {
|
|||
|
|
Self {
|
|||
|
|
flat_per_order: 0.0,
|
|||
|
|
rate_of_value: 0.001, // 0.1%
|
|||
|
|
per_lot: 0.0,
|
|||
|
|
max_brokerage: 20.0, // ₹20 cap
|
|||
|
|
spread_bps: 0.0,
|
|||
|
|
stt_rate: 0.001, // 0.1%
|
|||
|
|
stt_on_buy: true,
|
|||
|
|
stt_on_sell: true,
|
|||
|
|
exchange_charges_rate: 0.0000297,
|
|||
|
|
regulatory_charges_rate: 0.000001,
|
|||
|
|
gst_rate: 0.18,
|
|||
|
|
stamp_duty_rate: 0.00015,
|
|||
|
|
lot_size: 1.0,
|
|||
|
|
short_borrow_rate_annual: 0.0,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Indian equity **intraday** (same-day square-off).
|
|||
|
|
///
|
|||
|
|
/// Brokerage: 0.03% (capped at ₹20), STT 0.025% sell side only,
|
|||
|
|
/// exchange charges, regulatory body charges, 18% GST, stamp duty on buy.
|
|||
|
|
pub fn equity_intraday_india() -> Self {
|
|||
|
|
Self {
|
|||
|
|
flat_per_order: 0.0,
|
|||
|
|
rate_of_value: 0.0003, // 0.03%
|
|||
|
|
per_lot: 0.0,
|
|||
|
|
max_brokerage: 20.0,
|
|||
|
|
spread_bps: 0.0,
|
|||
|
|
stt_rate: 0.00025, // 0.025%
|
|||
|
|
stt_on_buy: false,
|
|||
|
|
stt_on_sell: true,
|
|||
|
|
exchange_charges_rate: 0.0000297,
|
|||
|
|
regulatory_charges_rate: 0.000001,
|
|||
|
|
gst_rate: 0.18,
|
|||
|
|
stamp_duty_rate: 0.000003,
|
|||
|
|
lot_size: 1.0,
|
|||
|
|
short_borrow_rate_annual: 0.0,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Indian **index futures** (indicative rates per current regulations).
|
|||
|
|
///
|
|||
|
|
/// Flat ₹20 per order, STT 0.05% sell side only, exchange charges,
|
|||
|
|
/// regulatory body charges, 18% GST, stamp duty on buy.
|
|||
|
|
/// `lot_size` defaults to 25 — update as needed for the specific contract.
|
|||
|
|
pub fn futures_india() -> Self {
|
|||
|
|
Self {
|
|||
|
|
flat_per_order: 20.0,
|
|||
|
|
rate_of_value: 0.0,
|
|||
|
|
per_lot: 0.0,
|
|||
|
|
max_brokerage: 0.0,
|
|||
|
|
spread_bps: 0.0,
|
|||
|
|
stt_rate: 0.0005, // 0.05%
|
|||
|
|
stt_on_buy: false,
|
|||
|
|
stt_on_sell: true,
|
|||
|
|
exchange_charges_rate: 0.0000019,
|
|||
|
|
regulatory_charges_rate: 0.000001,
|
|||
|
|
gst_rate: 0.18,
|
|||
|
|
stamp_duty_rate: 0.00002,
|
|||
|
|
lot_size: 25.0,
|
|||
|
|
short_borrow_rate_annual: 0.0,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Indian **index options** (indicative rates per current regulations).
|
|||
|
|
///
|
|||
|
|
/// Flat ₹20 per order, STT 0.15% on premium sell side only, exchange charges,
|
|||
|
|
/// regulatory body charges, 18% GST, stamp duty on buy.
|
|||
|
|
/// `lot_size` defaults to 25 — update as needed for the specific contract.
|
|||
|
|
pub fn options_india() -> Self {
|
|||
|
|
Self {
|
|||
|
|
flat_per_order: 20.0,
|
|||
|
|
rate_of_value: 0.0,
|
|||
|
|
per_lot: 0.0,
|
|||
|
|
max_brokerage: 0.0,
|
|||
|
|
spread_bps: 0.0,
|
|||
|
|
stt_rate: 0.0015, // 0.15% on premium
|
|||
|
|
stt_on_buy: false,
|
|||
|
|
stt_on_sell: true,
|
|||
|
|
exchange_charges_rate: 0.0000053,
|
|||
|
|
regulatory_charges_rate: 0.000001,
|
|||
|
|
gst_rate: 0.18,
|
|||
|
|
stamp_duty_rate: 0.000003,
|
|||
|
|
lot_size: 25.0,
|
|||
|
|
short_borrow_rate_annual: 0.0,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Simple proportional model — e.g., `proportional(0.001)` = 0.1% both sides.
|
|||
|
|
///
|
|||
|
|
/// No taxes, no levies — suitable for non-Indian markets or simplified modelling.
|
|||
|
|
pub fn proportional(rate: f64) -> Self {
|
|||
|
|
Self {
|
|||
|
|
rate_of_value: rate,
|
|||
|
|
..Default::default()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------
|
|||
|
|
// JSON serialization (requires "serde" feature)
|
|||
|
|
// ------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
/// Serialize to a pretty-printed JSON string.
|
|||
|
|
#[cfg(feature = "serde")]
|
|||
|
|
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
|||
|
|
serde_json::to_string_pretty(self)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Deserialize from a JSON string.
|
|||
|
|
#[cfg(feature = "serde")]
|
|||
|
|
pub fn from_json(s: &str) -> Result<Self, serde_json::Error> {
|
|||
|
|
serde_json::from_str(s)
|
|||
|
|
}
|
|||
|
|
}
|