def _get_forecast_method_buffer(system, instrument_code, this_stage): this_stage.log.msg("Calculating forecast method buffers for %s" % instrument_code, instrument_code=instrument_code) buffer_size=system.config.buffer_size idm = this_stage.get_instrument_diversification_multiplier() instr_weights = this_stage.get_instrument_weights() vol_scalar = this_stage.get_volatility_scalar( instrument_code) inst_weight_this_code = instr_weights[ instrument_code].to_frame("weight") inst_weight_this_code = inst_weight_this_code.reindex( vol_scalar.index).ffill() idm = idm.reindex(vol_scalar.index).ffill() multiplier = multiply_df_single_column(inst_weight_this_code, idm) average_position = multiply_df_single_column( vol_scalar, multiplier) buffer = average_position * buffer_size buffer.columns=["buffer"] return buffer
def _get_forecast_method_buffer(system, instrument_code, this_stage): this_stage.log.msg("Calculating forecast method buffers for %s" % instrument_code, instrument_code=instrument_code) buffer_size = system.config.buffer_size idm = this_stage.get_instrument_diversification_multiplier() instr_weights = this_stage.get_instrument_weights() vol_scalar = this_stage.get_volatility_scalar(instrument_code) inst_weight_this_code = instr_weights[instrument_code].to_frame( "weight") inst_weight_this_code = inst_weight_this_code.reindex( vol_scalar.index).ffill() idm = idm.reindex(vol_scalar.index).ffill() multiplier = multiply_df_single_column(inst_weight_this_code, idm) average_position = multiply_df_single_column( vol_scalar, multiplier) buffer = average_position * buffer_size buffer.columns = ["buffer"] return buffer
def get_instrument_scaling_factor(self, instrument_code): """ Get instrument weight * IDM The number we multiply subsystem by to get position Used to calculate SR costs :param instrument_code: instrument to value for :type instrument_code: str :returns: Tx1 pd.DataFrame """ idm = self.get_instrument_diversification_multiplier() instr_weights = self.get_instrument_weights() inst_weight_this_code = instr_weights[ instrument_code].to_frame("weight") multiplier = multiply_df_single_column(inst_weight_this_code, idm, ffill=(True, True)) return multiplier
def get_instrument_scaling_factor(self, instrument_code): """ Get instrument weight * IDM The number we multiply subsystem by to get position Used to calculate SR costs :param instrument_code: instrument to value for :type instrument_code: str :returns: Tx1 pd.DataFrame """ idm = self.get_instrument_diversification_multiplier() instr_weights = self.get_instrument_weights() inst_weight_this_code = instr_weights[instrument_code].to_frame( "weight") multiplier = multiply_df_single_column(inst_weight_this_code, idm, ffill=(True, True)) return multiplier
def _get_instrument_currency_vol(system, instrument_code, this_stage): block_value = this_stage.get_block_value(instrument_code) daily_perc_vol = this_stage.get_price_volatility(instrument_code) instr_ccy_vol = multiply_df_single_column( block_value, daily_perc_vol, ffill=(True, False)) instr_ccy_vol.columns = ['icv'] return instr_ccy_vol
def _get_notional_position(system, instrument_code, this_stage): idm = this_stage.get_instrument_diversification_multiplier() instr_weights = this_stage.get_instrument_weights() subsys_position = this_stage.get_subsystem_position( instrument_code) inst_weight_this_code = instr_weights[ instrument_code].to_frame("weight") inst_weight_this_code = inst_weight_this_code.reindex( subsys_position.index).ffill() idm = idm.reindex(subsys_position.index).ffill() multiplier = multiply_df_single_column(inst_weight_this_code, idm) notional_position = multiply_df_single_column( subsys_position, multiplier) notional_position.columns = ['pos'] return notional_position
def _instrument_turnover( system, instrument_code, this_stage, roundpositions): average_position_for_turnover=multiply_df_single_column( this_stage.get_volatility_scalar(instrument_code), this_stage.get_instrument_scaling_factor(instrument_code), ffill=(True, True)) positions = this_stage.get_buffered_position(instrument_code, roundpositions = roundpositions) return turnover(positions, average_position_for_turnover)
def _get_instrument_value_vol(system, instrument_code, this_stage): instr_ccy_vol = this_stage.get_instrument_currency_vol( instrument_code) fx_rate = this_stage.get_fx_rate(instrument_code) instr_value_vol = multiply_df_single_column( instr_ccy_vol, fx_rate, ffill=(False, True)) instr_value_vol.columns = ['ivv'] return instr_value_vol
def _instrument_turnover(system, instrument_code, this_stage, roundpositions): average_position_for_turnover = multiply_df_single_column( this_stage.get_volatility_scalar(instrument_code), this_stage.get_instrument_scaling_factor(instrument_code), ffill=(True, True)) positions = this_stage.get_buffered_position( instrument_code, roundpositions=roundpositions) return turnover(positions, average_position_for_turnover)
def _get_instrument_currency_vol(system, instrument_code, this_stage): this_stage.log.msg("Calculating instrument currency vol for %s" % instrument_code, instrument_code=instrument_code) block_value = this_stage.get_block_value(instrument_code) daily_perc_vol = this_stage.get_price_volatility(instrument_code) instr_ccy_vol = multiply_df_single_column( block_value, daily_perc_vol, ffill=(True, False)) instr_ccy_vol.columns = ['icv'] return instr_ccy_vol
def _get_instrument_value_vol(system, instrument_code, this_stage): this_stage.log.msg("Calculating instrument value vol for %s" % instrument_code, instrument_code=instrument_code) instr_ccy_vol = this_stage.get_instrument_currency_vol( instrument_code) fx_rate = this_stage.get_fx_rate(instrument_code) instr_value_vol = multiply_df_single_column( instr_ccy_vol, fx_rate, ffill=(False, True)) instr_value_vol.columns = ['ivv'] return instr_value_vol
def _get_subsystem_position(system, instrument_code, this_stage): """ We don't allow this to be changed in config """ avg_abs_forecast = system_defaults['average_absolute_forecast'] vol_scalar = this_stage.get_volatility_scalar(instrument_code) forecast = this_stage.get_combined_forecast(instrument_code) subsystem_position = multiply_df_single_column( vol_scalar, forecast, ffill=(True, False)) / avg_abs_forecast subsystem_position.columns = ['ss_position'] return subsystem_position
def _get_notional_position(system, instrument_code, this_stage): this_stage.log.msg("Calculating notional position for %s" % instrument_code, instrument_code=instrument_code) idm = this_stage.get_instrument_diversification_multiplier() instr_weights = this_stage.get_instrument_weights() subsys_position = this_stage.get_subsystem_position( instrument_code) inst_weight_this_code = instr_weights[instrument_code].to_frame( "weight") inst_weight_this_code = inst_weight_this_code.reindex( subsys_position.index).ffill() idm = idm.reindex(subsys_position.index).ffill() multiplier = multiply_df_single_column(inst_weight_this_code, idm) notional_position = multiply_df_single_column( subsys_position, multiplier) notional_position.columns = ['pos'] return notional_position
def _get_scaled_forecast( system, instrument_code, rule_variation_name, this_stage): raw_forecast = this_stage.get_raw_forecast( instrument_code, rule_variation_name) scale = this_stage.get_forecast_scalar( instrument_code, rule_variation_name) if type(scale) is float: scaled_forecast = raw_forecast * scale else: ## time series scaled_forecast = multiply_df_single_column(raw_forecast, scale, ffill=(False,True)) return scaled_forecast
def _get_scaled_forecast(system, instrument_code, rule_variation_name, this_stage): raw_forecast = this_stage.get_raw_forecast(instrument_code, rule_variation_name) scale = this_stage.get_forecast_scalar(instrument_code, rule_variation_name) if type(scale) is float: scaled_forecast = raw_forecast * scale else: ## time series scaled_forecast = multiply_df_single_column(raw_forecast, scale, ffill=(False, True)) return scaled_forecast
def _get_subsystem_position(system, instrument_code, this_stage): this_stage.log.msg("Calculating subsystem position for %s" % instrument_code, instrument_code=instrument_code) """ We don't allow this to be changed in config """ avg_abs_forecast = system_defaults['average_absolute_forecast'] vol_scalar = this_stage.get_volatility_scalar(instrument_code) forecast = this_stage.get_combined_forecast(instrument_code) subsystem_position = multiply_df_single_column( vol_scalar, forecast, ffill=(True, False)) / avg_abs_forecast subsystem_position.columns = ['ss_position'] return subsystem_position
def calc_costs(returns_data, cost_per_block, SR_cost, daily_capital): """ Calculate costs :param returns_data: returns data :type returns_data: 4 tuple returned by pandl data function :param cost_per_block: Cost in local currency units per instrument block :type cost_per_block: float :param SR_cost: Cost in annualised Sharpe Ratio units (0.01 = 0.01 SR) :type SR_cost: float If both included use SR_cost :param daily_capital: Capital at risk each day. Used for SR calculations :type daily_capital: Tx1 pd.DataFrame :returns : Tx1 pd.DataFrame of costs. Minus numbers are losses """ (cum_trades, trades, instr_ccy_returns, base_ccy_returns, fx)=returns_data if SR_cost is not None: ## use SR_cost ann_risk = daily_capital*ROOT_BDAYS_INYEAR ann_cost = -SR_cost*ann_risk costs_instr_ccy = ann_cost/BUSINESS_DAYS_IN_YEAR elif cost_per_block is not None: ## use cost per blocks trades_in_blocks=trades['trades'].abs().resample("1B", how="sum") costs_instr_ccy= - trades_in_blocks*cost_per_block else: ## set costs to zero costs_instr_ccy=pd.DataFrame([0.0]*base_ccy_returns.shape[0], index=base_ccy_returns.index) costs_base_ccy=multiply_df_single_column(costs_instr_ccy, fx, ffill=(False, True)) return (costs_base_ccy, costs_instr_ccy)
def get_forecast_scaling_factor(self, instrument_code, rule_variation_name): """ Get forecast weight * FDM :param instrument_code: instrument to value for :type instrument_code: str :returns: Tx1 pd.DataFrame """ fdm = self.get_forecast_diversification_multiplier(instrument_code) forecast_weights = self.get_forecast_weights(instrument_code) fcast_weight_this_code = forecast_weights[ rule_variation_name].to_frame("weight") multiplier = multiply_df_single_column(fcast_weight_this_code, fdm, ffill=(True, True)) return multiplier
def get_positions_from_forecasts(price, get_daily_returns_volatility, forecast, fx, value_of_price_point, capital, ann_risk_target, **kwargs): """ Work out position using forecast, volatility, fx, value_of_price_point (this will be for an arbitrary daily risk target) If volatility is not provided, work out from price (uses a standard method so may differ from precise system p&l) :param price: price series :type price: Tx1 pd.DataFrame :param get_daily_returns_volatility: series of volatility estimates. NOT % volatility, price difference vol :type get_daily_returns_volatility: Tx1 pd.DataFrame or None :param forecast: series of forecasts, needed to work out positions :type forecast: Tx1 pd.DataFrame :param fx: series of fx rates from instrument currency to base currency, to work out p&l in base currency :type fx: Tx1 pd.DataFrame :param value_of_price_point: value of one unit movement in price :type value_of_price_point: float **kwargs: passed to vol calculation :returns: Tx1 pd dataframe of positions """ if forecast is None: raise Exception( "If you don't provide a series of trades or positions, I need a " "forecast") if get_daily_returns_volatility is None: get_daily_returns_volatility = robust_vol_calc(price.diff(), **kwargs) """ Herein the proof why this position calculation is correct (see chapters 5-11 of 'systematic trading' book) Position = forecast x instrument weight x instrument_div_mult x vol_scalar / 10.0 = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x instr value volatility) = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x instr ccy volatility x fx rate) = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x block value x % price volatility x fx rate) = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x underlying price x 0.01 x value of price move x 100 x price change volatility/(underlying price) x fx rate) = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x value of price move x price change volatility x fx rate) Making some arbitrary assumptions (one instrument, 100% of capital, daily target DAILY_CAPITAL): = forecast x 1.0 x 1.0 x DAILY_CAPITAL / (10.0 x value of price move x price diff volatility x fx rate) = forecast x multiplier / (value of price move x price change volatility x fx rate) """ (Unused_capital, daily_capital) = resolve_capital(forecast, capital, ann_risk_target) multiplier = daily_capital * 1.0 * 1.0 / 10.0 fx = fx.reindex(get_daily_returns_volatility.index, method="ffill") denominator = (value_of_price_point * multiply_df_single_column(get_daily_returns_volatility, fx, ffill=(False, True))) numerator = multiply_df_single_column(forecast, multiplier, ffill=(False,True)) position = divide_df_single_column(numerator, denominator, ffill=(True, True)) position.columns = ['position'] return position
def get_positions_from_forecasts(price, get_daily_returns_volatility, forecast, fx, value_of_price_point, **kwargs): """ Work out position using forecast, volatility, fx, value_of_price_point (this will be for an arbitrary daily risk target) If volatility is not provided, work out from price (uses a standard method so may differ from precise system p&l) :param price: price series :type price: Tx1 pd.DataFrame :param get_daily_returns_volatility: series of volatility estimates. NOT % volatility, price difference vol :type get_daily_returns_volatility: Tx1 pd.DataFrame or None :param forecast: series of forecasts, needed to work out positions :type forecast: Tx1 pd.DataFrame :param fx: series of fx rates from instrument currency to base currency, to work out p&l in base currency :type fx: Tx1 pd.DataFrame :param value_of_price_point: value of one unit movement in price :type value_of_price_point: float **kwargs: passed to vol calculation :returns: Tx1 pd dataframe of positions """ if forecast is None: raise Exception( "If you don't provide a series of trades or positions, I need a " "forecast") if get_daily_returns_volatility is None: get_daily_returns_volatility = robust_vol_calc(price.diff(), **kwargs) """ Herein the proof why this position calculation is correct (see chapters 5-11 of 'systematic trading' book) Position = forecast x instrument weight x instrument_div_mult x vol_scalar / 10.0 = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x instr value volatility) = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x instr ccy volatility x fx rate) = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x block value x % price volatility x fx rate) = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x underlying price x 0.01 x value of price move x 100 x price diff volatility/(underlying price) x fx rate) = forecast x instrument weight x instrument_div_mult x daily cash vol target / (10.0 x value of price move x price change volatility x fx rate) Making some arbitrary assumptions (one instrument, 100% of capital, daily target DAILY_CAPITAL): = forecast x 1.0 x 1.0 x DAILY_CAPITAL / (10.0 x value of price move x price diff volatility x fx rate) = forecast x multiplier / (value of price move x price change volatility x fx rate) """ multiplier = DAILY_CAPITAL * 1.0 * 1.0 / 10.0 fx = fx.reindex(get_daily_returns_volatility.index, method="ffill") denominator = (value_of_price_point * multiply_df_single_column( get_daily_returns_volatility, fx, ffill=(False, True))) position = divide_df_single_column(forecast * multiplier, denominator, ffill=(True, True)) position.columns = ['position'] return position
def pandl(price=None, trades=None, marktomarket=True, positions=None, delayfill=True, roundpositions=False, get_daily_returns_volatility=None, forecast=None, fx=None, value_of_price_point=1.0, return_all=False, capital=None): """ Calculate pandl for an individual position If marktomarket=True, and trades is provided, calculate pandl both at open/close and mark to market in between If trades is not provided, work out using positions. If delayfill is True, assume we get filled at the next price after the trade If roundpositions is True when working out trades from positions, then round; otherwise assume we trade fractional lots If positions are not provided, work out position using forecast and volatility (this will be for an arbitrary daily risk target) If volatility is not provided, work out from price If fx is not provided, assume fx rate is 1.0 and work out p&l in currency of instrument If value_of_price_point is not provided, assume is 1.0 (block size is value of 1 price point, eg 100 if you're buying 100 shares for one instrument block) If capital is provided (eithier as a float, or dataframe) then % returns will be calculated. If capital is zero will use default values :param price: price series :type price: Tx1 pd.DataFrame :param trades: set of trades done :type trades: Tx1 pd.DataFrame or None :param marktomarket: Should we mark to market, or just use traded prices? :type marktomarket: bool :param positions: series of positions :type positions: Tx1 pd.DataFrame or None :param delayfill: If calculating trades, should we round positions first? :type delayfill: bool :param roundpositions: If calculating trades, should we round positions first? :type roundpositions: bool :param get_daily_returns_volatility: series of volatility estimates, used for calculation positions :type get_daily_returns_volatility: Tx1 pd.DataFrame or None :param forecast: series of forecasts, needed to work out positions :type forecast: Tx1 pd.DataFrame or None :param fx: series of fx rates from instrument currency to base currency, to work out p&l in base currency :type fx: Tx1 pd.DataFrame or None :param value_of_price_point: value of one unit movement in price :type value_of_price_point: float :param roundpositions: If calculating trades, should we round positions first? :type roundpositions: bool :param capital: notional capital. If None not used. Works out % returns. If 0.0 uses default :type capital: None, 0.0, float or Tx1 timeseries :returns: if return_all : 4- Tuple (positions, trades, instr_ccy_returns, base_ccy_returns) all Tx1 pd.DataFrames is "": Tx1 accountCurve """ if price is None: raise Exception("Can't work p&l without price") if fx is None: # assume it's 1.0 fx = pd.Series([1.0] * len(price.index), index=price.index).to_frame("fx") if trades is None: trades = get_trades_from_positions(price, positions, delayfill, roundpositions, get_daily_returns_volatility, forecast, fx, value_of_price_point) if marktomarket: # want to have both kinds of price prices_to_use = pd.concat([price, trades.fill_price], axis=1, join='outer') # Where no fill price available, use price prices_to_use = prices_to_use.fillna(axis=1, method="ffill") prices_to_use = prices_to_use.fill_price.to_frame("price") # alight trades trades_to_use = trades.reindex( prices_to_use.index, fill_value=0.0).trades.to_frame("trades") else: # only calculate p&l on trades, using fills trades_to_use = trades.trades.to_frame("trades") prices_to_use = trades.fill_price.to_frame("price").ffill() cum_trades = trades_to_use.cumsum().ffill() price_returns = prices_to_use.diff() instr_ccy_returns = multiply_df_single_column( cum_trades.shift(1), price_returns) * value_of_price_point fx = fx.reindex(trades_to_use.index, method="ffill") base_ccy_returns = multiply_df_single_column(instr_ccy_returns, fx) instr_ccy_returns.columns = ["pandl_ccy"] base_ccy_returns.columns = ["pandl_base"] cum_trades.columns = ["cum_trades"] if return_all: return (cum_trades, trades, instr_ccy_returns, base_ccy_returns, capital) else: if capital is not None: if isinstance(capital, float): if capital == 0.0: # use default. Good for forecasts when no meaningful # capital capital = CAPITAL base_ccy_returns = base_ccy_returns / capital else: # time series capital = capital.reindex(base_ccy_returns.index, method="ffill") base_ccy_returns = divide_df_single_column( base_ccy_returns, capital) return accountCurve(base_ccy_returns)
def pandl_with_data(price, trades=None, marktomarket=True, positions=None, delayfill=True, roundpositions=False, get_daily_returns_volatility=None, forecast=None, fx=None, capital=None, ann_risk_target=None, value_of_price_point=1.0): """ Calculate pandl for an individual position If marktomarket=True, and trades is provided, calculate pandl both at open/close and mark to market in between If trades is not provided, work out using positions. If delayfill is True, assume we get filled at the next price after the trade If roundpositions is True when working out trades from positions, then round; otherwise assume we trade fractional lots If positions are not provided, work out position using forecast and volatility (this will be for an arbitrary daily risk target) If volatility is not provided, work out from price If fx is not provided, assume fx rate is 1.0 and work out p&l in currency of instrument If value_of_price_point is not provided, assume is 1.0 (block size is value of 1 price point, eg 100 if you're buying 100 shares for one instrument block) :param price: price series :type price: Tx1 pd.DataFrame :param trades: set of trades done :type trades: Tx1 pd.DataFrame or None :param marktomarket: Should we mark to market, or just use traded prices? :type marktomarket: bool :param positions: series of positions :type positions: Tx1 pd.DataFrame or None :param delayfill: If calculating trades, should we round positions first? :type delayfill: bool :param roundpositions: If calculating trades, should we round positions first? :type roundpositions: bool :param get_daily_returns_volatility: series of volatility estimates, used for calculation positions :type get_daily_returns_volatility: Tx1 pd.DataFrame or None :param forecast: series of forecasts, needed to work out positions :type forecast: Tx1 pd.DataFrame or None :param fx: series of fx rates from instrument currency to base currency, to work out p&l in base currency :type fx: Tx1 pd.DataFrame or None :param value_of_price_point: value of one unit movement in price :type value_of_price_point: float :param roundpositions: If calculating trades, should we round positions first? :type roundpositions: bool :returns: 5- Tuple (positions, trades, instr_ccy_returns, base_ccy_returns, fx) all Tx1 pd.DataFrames """ if price is None: raise Exception("Can't work p&l without price") if fx is None: # assume it's 1.0 fx = pd.Series([1.0] * len(price.index), index=price.index).to_frame("fx") if trades is None: trades = get_trades_from_positions(price, positions, delayfill, roundpositions, get_daily_returns_volatility, forecast, fx, value_of_price_point, capital, ann_risk_target) if marktomarket: # want to have both kinds of price prices_to_use = pd.concat( [price, trades.fill_price], axis=1, join='outer') # Where no fill price available, use price prices_to_use = prices_to_use.fillna(axis=1, method="ffill") prices_to_use = prices_to_use.fill_price.to_frame("price") # alight trades trades_to_use = trades.reindex( prices_to_use.index, fill_value=0.0).trades.to_frame("trades") else: # only calculate p&l on trades, using fills trades_to_use = trades.trades.to_frame("trades") prices_to_use = trades.fill_price.to_frame("price").ffill() cum_trades = trades_to_use.cumsum().ffill() price_returns = prices_to_use.diff() instr_ccy_returns = multiply_df_single_column( cum_trades.shift(1), price_returns) * value_of_price_point instr_ccy_returns=instr_ccy_returns.resample("1B", how="sum") fx = fx.reindex(instr_ccy_returns.index, method="ffill") base_ccy_returns = multiply_df_single_column(instr_ccy_returns, fx) instr_ccy_returns.columns = ["pandl_ccy"] base_ccy_returns.columns = ["pandl_base"] cum_trades.columns = ["cum_trades"] return (cum_trades, trades, instr_ccy_returns, base_ccy_returns, fx)