Transaction cost#
This page shows how transactions costs can be altered.
When running strategies, transactions costs are not considered by default. You can include transactions costs by running the following code.
import sigtech.framework as sig
env = sig.init()
env[sig.config.IGNORE_T_COSTS] = False
There are default transaction costs defined either on the TradePricer
class or on the instrument class. These can be overridden when the environment is initialised.
A transaction cost model can be set by its instrument classname, its instrument group or its individual ticker. If the transaction model is not defined in the register, it defaults to the model defined on the instrument class.
The parameters used in the transaction cost can be updated or a new transaction model with new models can be passed.
The overrides passed are a dictionary with the following keys:
Class name
Group ticker
Instrument ticker
The values of the dictionary are a tuple with the first value being the name of the transaction model to use and the second any parameters the model might use.
Specifically, we can assign transaction cost models to instruments based on the following criteria:
Class name, such as
SingleStock
,Future
orCommodityFuture
.Group ticker, such as
ES COMDTY FUTURES GROUP
orUS GOVT BOND GROUP
.Instrument ticker, such as
1000045.SINGLE_STOCK.TRADABLE
,ESH18 COMDTY
orUS 2.5 2006/10/31 GOVT
.
Note: the above criteria is ordered by ascending specificity of scope.
Learn more: Example notebooks
Environment#
This section will import relevant internal and external libraries, as well as setting up the platform environment.
Learn more: Environment setup
import sigtech.framework as sig
import pytz
import datetime as dtm
if not sig.config.is_initialised():
sig.init()
sig.config.set(sig.config.HISTORY_SCHEDULE_BUILD_FROM_DATA, True)
# Here is where the transaction costs overrides are set
sig.config.set(sig.config.T_COST_OVERRIDES, {
'TestEquity': ('fixed_cost', 2),
'US GOVT BOND GROUP': ('yield_transaction_type', None),
'TEST_2 EQUITY': ('foo', {'long_multiple': 1.2, 'short_multiple': 0.95}),
'EURUSD CURNCY': ('spread_from_mid', {'spread': 0.0005}),
'EURUSD OTC OPTION GROUP': ('option_default', None),
'GBPUSD OTC OPTION GROUP': ('option_vol_spread', 0.001),
})
Transaction cost models available by default #
A number of basic transaction cost models are available on the platform—they need to be passed calibrated parameters to function.
Background #
Some glossary terms:
Cost versus arrival price \(C\): represents the cost incurred on a trade, expressed as a percentage of price. Example: if \(C = 0.002\) and bid, or ask, price is \(£35.22\), then cost incurred would be ca. \(£0.07\).
Participation rate \(r\): \(\frac{Q}{V}\) where \(Q\) is the order size, in units, and \(V\) is a measure of the average daily volume, in units. \(r\) is the proportion of the daily volume that is constituted by this trade.
Cost of transacting (\(C\)) is typically characterised as containing three parts:
Note: trade cost (\(C\)) = instant impact + temporary impact + permanent impact
Instant impact: cost incurred immediately, such as crossing the bid/offer spread or incurring slippage.
Temporary impact: adverse market price movement during the execution of the trade.
Permanent impact: difference in market price before and after the trade.
The temporary and permanent impacts are sometimes grouped together under the market impact.
Fixed commission #
Applies a fixed commission to the trade, irrespective of quantity traded or price of instrument:
sig.config.set(sig.config.T_COST_OVERRIDES, {
'Instrument_scope_here': ('fixed_commission', 6.5),
})
Result: applies a fixed 6.50 units of cost to the trade, in the currency of the trade.
Fixed cost (fixed spread from mid) #
Applies a fixed cost from the mid of the instrument:
sig.config.set(sig.config.T_COST_OVERRIDES, {
'Instrument_scope_here': ('fixed_cost', 2),
})
Result: applies a cost of 2 units of currency from the mid of the instrument.
Fixed relative cost (percentage spread from mid) #
Applies a fixed cost relative to the mid of the instrument. 0.01
would equate to a charge of 1% of mid.
sig.config.set(sig.config.T_COST_OVERRIDES, {
'Instrument_scope_here': ('fixed_relative_cost', 0.002),
})
Result: applies a cost of 20 bps spread from mid to the trade.
Square-root market impact model #
Cost is represented as only a market impact, expressed as:
Where \(\sigma\) is the daily volatility of returns and where \(c\) and \(a\) are some constants of \(\mathcal{O}(1)\). Sotiropoulos et. al (2018) suggest \(c ≈ 0.35\) and \(a ≈ 0.40\) for the U.S. equity markets.
This model can also be thought of as a spread varying with volatility (the 𝑐𝜎cσ term) and proportional to the participation rate raised to some power (the \(r^\alpha \)term).
Usage 1:
sig.config.set(sig.config.T_COST_OVERRIDES, {
'Instrument_scope_here': ('market_impact_square_root', (0.35, 0.40)),
})
Result: applies a cost of \(c \sigma r^\alpha\) where \(c = 0.35\) and \(a = 0.40\).
Usage 2:
sig.config.set(sig.config.T_COST_OVERRIDES, {
'Instrument_scope_here': ('market_impact_square_root', (0.35, 0.40, 0.012)),
})
Result: applies a cost of \(c \sigma r^\alpha\), where \(c = 0.35\), \(a = 0.40\) and daily volatility \(\sigma\) is set to 1.2%.
Combined model (square-root market impact plus spread relative to mid) #
Cost is represented as a combination of a spread and a market impact:
Usage:
sig.config.set(sig.config.T_COST_OVERRIDES, {
'Instrument_scope_here': ('combined_spread_market_impact', (0.004, 0.33, 0.41)),
})
Result: applies a cost of \(k + c \sigma r^\alpha\) where \(k = 0.004\), \(c = 0.33\) and \(a = 0.41\). This would be equivalent to a 40 bps spread plus a square-root market impact type cost with the given values for \(c\) and \(a\).
Building your own transaction cost model #
Clients often have detailed and specific transaction cost data and models and wish to integrate these into the platform. The SigTech platform’s flexible transaction cost API makes this as simple as building a method that takes inputs and returns a final trade price.
Anatomy of a transaction cost model #
When a trade is to be executed, the platform goes through the following steps:
Using the instrument name, look in
T_COST_OVERRIDES
for a given method to run and parameters to pass to mentioned method. If none found, fall back to the default set of methods.Run the method with the parameters stored in
T_COST_OVERRIDES
.Inside method: execute instructions inside method, having access to all information stored in the parent class (more information below).
Inside method: return the price \(p\) at which the trade will be executed.
Book trade at price \(p\).
A client’s custom transaction cost model would need to be called by this method and make use of the inputs given by the parent class.
Creating a transaction cost model #
Creating a transaction cost model is as simple as defining a new method and attaching this method to either the TradePricer
, for non-FX instruments, or FxPricer
, for FX instruments:
def my_transaction_cost_model(self):
""" A custom transaction cost model that multiplies the price of an instrument by a certain factor. """
# Gets gross price
base_price = self.instrument.valuation_price_base(self.d)
# Depending on whether we are long or short, return different multiples
if self.quantity >= 0:
return base_price * self.params['multiple_long']
else:
return base_price * self.params['multiple_short']
sig.TradePricer.my_transaction_cost_model = my_transaction_cost_model
The custom transaction cost model is saved and can be set in the sig.config.T_COST_OVERRIDES
:
sig.config.set(sig.config.T_COST_OVERRIDES, {
'Instrument_scope_here': ('my_transaction_cost_model', {'multiple_long': 1.1, 'multiple_short': 1/1.1}),
})
The model takes one parameter, namely multiple
, which is passed in through a dictionary.
Inputs: information available to a custom transaction cost model #
A custom transaction cost model can make use of any of the properties existing in the parent class, TradePricer
:
Instrument name to trade
datetime
of tradeQuantity to trade
Currency of trade
Transaction type, such as outright and roll.
Tip: to see a full list, view the source code for TradePricer
by querying sig.TradePricer??
.
FX-specific transaction costs #
The FX spot and forward transaction costs can be customised using the same T_COST_OVERRIDES
dictionary.
Due to how the FX time series is cached in the platform, the transaction costs applied to FX instruments are done so through the FxPricer
class, rather than the TradePricer
.
Note: transaction costs set in this way act on the entire history rather than one time point, which is the main difference.
Note: only spread_from_mid
is currently supported.
Set by currency cross #
sig.config.set(sig.config.T_COST_OVERRIDES, {
'EURUSD CURNCY': ('spread_from_mid_relative', {'spread': 0.0005}),
})
This sets the EURUSD pair to use the spread_from_mid
transaction model passing the parameter 0.0005
. This results in a 5 bps, or 0.05%, transaction cost (from mid) being applied.
Input:
sig.TradePricer.spread_from_mid??
Output:
Signature: sig.TradePricer.spread_from_mid(self, t_cost=None)
Docstring:
Price computed as mid price plus/minus spread.
:param t_cost: Transaction cost.
File: ~/framework/framework/sigtech/framework/transaction_cost/transaction_cost.py
Type: function
Use case: custom option transaction cost model #
The default behaviour is to take the bid/ask from the market vol surface data. The following is an example of doing the same with an override and how to apply a custom spread to the vol.
Adding the transaction cost methods #
Two new transaction cost methods are added to the TradePricer
in the following example:
Note: once they are added they can then be used as an override in the environment configuration.
def option_default(self):
""" Take bid/ask from the volsurface data - this is the default behaviour"""
print('-- Running option_default cost retrieval for {} unit --'.format(str(self.quantity)))
option = self.instrument
if self.quantity >= 0:
return option._trade_price(self.dt, currency=self.currency, mode='ask')
else:
return option._trade_price(self.dt, currency=self.currency, mode='bid')
def option_vol_spread(self):
print('-- Running option_vol_spread cost retrieval for {} unit --'.format(str(self.quantity)))
option = self.instrument
grp = option.group()
trade_date = self.d
# Find the mid vol for the option
mid_vol = grp.vol_from_strike(
option.option_type,
option.strike,
trade_date,
option.maturity_date,
mode='mid'
)
# Apply a spread to the vol based on the direction to trade
if self.quantity >= 0:
vol = mid_vol + self.params
mode = 'ASK'
else:
vol = mid_vol - self.params
mode = 'BID'
# Evaluate the option valuation
price = option.env.quant_bridge.black_calculator(
option._quant_library_option.option_info['payoff'],
trade_date,
option.maturity_date,
grp.spot_value(trade_date),
grp.forward_price(trade_date, option.maturity_date),
vol,
grp.discount_curve(trade_date),
grp.get_vol_surface().day_count,
['NPV'],
)
# Return the valuation adjusted for fx rates
return option.env.fx_cross.convert_at_d(
price['NPV'],
trade_date,
from_ccy=option.currency,
to_ccy=self.currency,
field=mode + 'Price'
)
# Add the method to the pricer
sig.TradePricer.option_default = option_default
sig.TradePricer.option_vol_spread = option_vol_spread
Testing the costs #
Once the config is initialised with the overrides, these costs are applied throughout the framework.
The following code block shows how to retrieve the prices used on examples of EURGBP
and GBPUSD
options. The vol surface will be loaded for the first pricing of an option in each group.
EURUSD
Input:
eurusd_fx_option_group = sig.obj.get('EURUSD OTC OPTION GROUP')
eurusd_call_option = eurusd_fx_option_group.get_option(
'Call',
1.3,
dtm.date(2010, 4, 6),
dtm.date(2011, 4, 6)
)
tz = pytz.timezone('Europe/London')
eurusd_ask = sig.TradePricer(
eurusd_call_option.name,
tz.localize(dtm.datetime(2010, 5, 6, 16)),
1,
eurusd_call_option.currency,
'outright'
).trade_price()
eurusd_bid = sig.TradePricer(
eurusd_call_option.name,
tz.localize(dtm.datetime(2010, 5, 6, 16)),
-1,
eurusd_call_option.currency,
'outright'
).trade_price()
print('{} : Ask {} - Bid {}'.format(
eurusd_call_option.name,
str(eurusd_ask),
str(eurusd_bid)
))
Output:
-- Running option_default cost retrieval for 1 unit --
-- Running option_default cost retrieval for -1 unit --
EURUSD 20110406 CALL 1.3 2432C0B2 CURNCY : Ask 0.05857657771580961 - Bid 0.05711512613362098
GBPUSD
Input:
gbpusd_fx_option_group = sig.obj.get('GBPUSD OTC OPTION GROUP')
gbpusd_call_option = gbpusd_fx_option_group.get_option(
'Call',
1.52,
dtm.date(2010, 4, 6),
dtm.date(2011, 4, 6)
)
gbpusd_ask = sig.TradePricer(
gbpusd_call_option.name,
tz.localize(dtm.datetime(2010, 5, 6, 15)),
1,
gbpusd_call_option.currency,
'outright'
).trade_price()
gbpusd_bid = sig.TradePricer(
gbpusd_call_option.name,
tz.localize(dtm.datetime(2010, 5, 6, 15)),
-1,
gbpusd_call_option.currency,
'outright'
).trade_price()
print('{} Ask {} - Bid {}'.format(
gbpusd_call_option.name,
str(gbpusd_ask),
str(gbpusd_bid)
))
Output:
-- Running option_vol_spread cost retrieval for 1 unit --
-- Running option_vol_spread cost retrieval for -1 unit --
GBPUSD 20110406 CALL 1.52 88628D24 CURNCY Ask 0.06781705290343404 - Bid 0.06668396352274569
Querying the transaction cost model#
The TradePricer interface allows you to get the information about the transaction cost model applied to a given instrument:
lme_future = sig.LMEFuture( underlying_ticker='LX',
future_ticker='K21',
strike=0)
sig.TradePricer.get_tc_model(lme_future.name, display=True)