def test_divide_df_single_column(self): x = pd.DataFrame(dict(a=[2.0, 7.0, -7.0, -7.00, 3.5]), pd.date_range(pd.datetime(2015, 1, 1), periods=5)) y = pd.DataFrame(dict(b=[2.0, 3.5, 2.0, -3.5, -3.5]), pd.date_range(pd.datetime(2015, 1, 1), periods=5)) ans = list(divide_df_single_column(x, y).iloc[:, 0]) self.assertEqual(ans, [1., 2., -3.5, 2., -1.]) x = pd.DataFrame(dict(a=[2.0, np.nan, -7.0, np.nan, 3.5]), pd.date_range(pd.datetime(2015, 1, 1), periods=5)) y = pd.DataFrame(dict(b=[2.0, 3.5, np.nan, np.nan, -3.5]), pd.date_range(pd.datetime(2015, 1, 2), periods=5)) ans = list(divide_df_single_column(x, y).iloc[:, 0]) self.assertTrue(np.isnan(ans[0])) self.assertTrue(np.isnan(ans[1])) self.assertTrue(np.isnan(ans[3])) self.assertEqual(ans[2], -2.0) ans = list(divide_df_single_column( x, y, ffill=(True, False)).iloc[:, 0]) self.assertEqual(ans[1], 1.0) ans = list(divide_df_single_column( x, y, ffill=(False, True)).iloc[:, 0]) self.assertEqual(ans[4], 1.0) ans = list(divide_df_single_column( x, y, ffill=(True, True)).iloc[:, 0]) self.assertEqual(list(ans)[1:], [1., -2., -2.0, 1., -1.])
def carry(daily_ann_roll, vol, smooth_days=90): """ Calculate raw carry forecast, given annualised roll and volatility series (which must match) Assumes that everything is daily data :param daily_ann_roll: The annualised roll :type daily_ann_roll: pd.DataFrame (assumed Tx1) :param vol: The daily price unit volatility (NOT % vol) :type vol: pd.DataFrame (assumed Tx1) >>> from systems.provided.example.testdata import get_test_object_futures >>> from systems.basesystem import System >>> (rawdata, data, config)=get_test_object_futures() >>> system=System( [rawdata], data, config) >>> >>> carry(rawdata.daily_annualised_roll("EDOLLAR"), rawdata.daily_returns_volatility("EDOLLAR")).tail(2) annualised_roll_daily 2015-04-21 0.350892 2015-04-22 0.350892 """ ann_stdev = vol * ROOT_BDAYS_INYEAR raw_carry = divide_df_single_column(daily_ann_roll, ann_stdev, ffill=(False, True)) smooth_carry = pd.ewma(raw_carry, smooth_days) return smooth_carry
def calc_ewmac_forecast(price, Lfast, Lslow=None): """ Calculate the ewmac trading fule forecast, given a price and EWMA speeds Lfast, Lslow and vol_lookback """ # price: This is the stitched price series # We can't use the price of the contract we're trading, or the volatility will be jumpy # And we'll miss out on the rolldown. See # http://qoppac.blogspot.co.uk/2015/05/systems-building-futures-rolling.html price=price.resample("1B", how="last") if Lslow is None: Lslow = 4 * Lfast # We don't need to calculate the decay parameter, just use the span # directly fast_ewma = pd.ewma(price, span=Lfast) slow_ewma = pd.ewma(price, span=Lslow) raw_ewmac = fast_ewma - slow_ewma vol = robust_vol_calc(price.diff()) return divide_df_single_column(raw_ewmac, vol)
def ewmac_forecast_with_defaults(price, Lfast=32, Lslow=128): """ Calculate the ewmac trading fule forecast, given a price and EWMA speeds Lfast, Lslow and vol_lookback Assumes that 'price' is daily data This version recalculates the price volatility, and does not do capping or scaling :param price: The price or other series to use (assumed Tx1) :type price: pd.DataFrame :param Lfast: Lookback for fast in days :type Lfast: int :param Lslow: Lookback for slow in days :type Lslow: int :returns: pd.DataFrame -- unscaled, uncapped forecast """ # price: This is the stitched price series # We can't use the price of the contract we're trading, or the volatility will be jumpy # And we'll miss out on the rolldown. See # http://qoppac.blogspot.co.uk/2015/05/systems-building-futures-rolling.html # We don't need to calculate the decay parameter, just use the span # directly fast_ewma = pd.ewma(price, span=Lfast) slow_ewma = pd.ewma(price, span=Lslow) raw_ewmac = fast_ewma - slow_ewma vol = robust_vol_calc(price.diff()) return divide_df_single_column(raw_ewmac, vol, ffill=(False, True))
def get_scatter_data_for_code_vol(system, instrument_code, rule_name, return_period=5, days=64): denom_price = system.rawdata.daily_denominator_price(instrument_code) x = system.rawdata.daily_returns(instrument_code) vol = robust_vol_calc(x, days) perc_vol = 100.0 * divide_df_single_column(vol, denom_price.shift(1)) volavg = pd.rolling_median(perc_vol, 1250, min_periods=10) vol_qq = (perc_vol - volavg) / volavg ## work out return for the N days after the forecast norm_data = system.accounts.pandl_for_instrument_forecast( instrument_code, rule_name) (vol_qq, norm_data) = align_to_joint(vol_qq, norm_data, ffill=(True, False)) period_returns = pd.rolling_sum(norm_data, return_period, min_periods=1) ex_post_returns = period_returns.shift(-return_period) lagged_vol = vol_qq.shift(1) return (list(ex_post_returns.iloc[:, 0].values), list(lagged_vol.iloc[:, 0].values))
def calc_ewmac_forecast(price, Lfast, Lslow=None): """ Calculate the ewmac trading fule forecast, given a price and EWMA speeds Lfast, Lslow and vol_lookback """ # price: This is the stitched price series # We can't use the price of the contract we're trading, or the volatility will be jumpy # And we'll miss out on the rolldown. See # http://qoppac.blogspot.co.uk/2015/05/systems-building-futures-rolling.html price = price.resample("1B", how="last") if Lslow is None: Lslow = 4 * Lfast # We don't need to calculate the decay parameter, just use the span # directly fast_ewma = pd.ewma(price, span=Lfast) slow_ewma = pd.ewma(price, span=Lslow) raw_ewmac = fast_ewma - slow_ewma vol = robust_vol_calc(price.diff()) return divide_df_single_column(raw_ewmac, vol)
def get_scatter_data_for_code_vol( system, instrument_code, rule_name, return_period=5, days=64): denom_price = system.rawdata.daily_denominator_price(instrument_code) x = system.rawdata.daily_returns(instrument_code) vol = robust_vol_calc(x, days) perc_vol = 100.0 * divide_df_single_column(vol, denom_price.shift(1)) volavg = pd.rolling_median(perc_vol, 1250, min_periods=10) vol_qq = (perc_vol - volavg) / volavg # work out return for the N days after the forecast norm_data = system.accounts.pandl_for_instrument_forecast( instrument_code, rule_name) (vol_qq, norm_data) = align_to_joint( vol_qq, norm_data, ffill=(True, False)) period_returns = pd.rolling_sum(norm_data, return_period, min_periods=1) ex_post_returns = period_returns.shift(-return_period) lagged_vol = vol_qq.shift(1) return (list(ex_post_returns.iloc[:, 0].values), list( lagged_vol.iloc[:, 0].values))
def _get_fx_cross(self, currency1, currency2): """ Get the FX rate between two currencies, using crosses with DEFAULT_CURRENCY if neccessary :param instrument_code: instrument to value for :type instrument_code: str :param base_currency: instrument to value for :type instrument_code: str :returns: Tx1 pd.DataFrame """ # try and get from raw data fx_rate_series = self._get_fx_data(currency1, currency2) if fx_rate_series is None: # missing; have to get get cross rates default_currency = self._get_default_currency() currency1_vs_default = self._get_fx_data( currency1, default_currency).resample("1B", how="last") currency2_vs_default = self._get_fx_data( currency2, default_currency).resample("1B", how="last") fx_rate_series = divide_df_single_column(currency1_vs_default, currency2_vs_default, ffill=(True, True)) fx_rate_series.columns = ['fx'] return fx_rate_series
def _get_fx_cross(self, currency1, currency2): """ Get the FX rate between two currencies, using crosses with DEFAULT_CURRENCY if neccessary :param instrument_code: instrument to value for :type instrument_code: str :param base_currency: instrument to value for :type instrument_code: str :returns: Tx1 pd.DataFrame """ # try and get from raw data fx_rate_series = self._get_fx_data(currency1, currency2) if fx_rate_series is None: # missing; have to get get cross rates default_currency = self._get_default_currency() currency1_vs_default = self._get_fx_data( currency1, default_currency).resample("1B", how="last") currency2_vs_default = self._get_fx_data( currency2, default_currency).resample("1B", how="last") fx_rate_series=divide_df_single_column(currency1_vs_default, currency2_vs_default, ffill=(True, True)) fx_rate_series.columns=['fx'] return fx_rate_series
def _norm_returns(system, instrument_code, this_stage): returnvol = this_stage.daily_returns_volatility( instrument_code).shift(1) dailyreturns = this_stage.daily_returns(instrument_code) norm_return = divide_df_single_column(dailyreturns, returnvol) norm_return.columns = ["norm_return"] return norm_return
def _get_daily_percentage_volatility( system, instrument_code, this_stage): denom_price = this_stage.daily_denominator_price(instrument_code) return_vol = this_stage.daily_returns_volatility(instrument_code) perc_vol = 100.0 * \ divide_df_single_column(return_vol, denom_price.shift(1)) return perc_vol
def _calc_annualised_roll(system, instrument_code, this_subsystem): rolldiffs = this_subsystem.roll_differentials(instrument_code) rawrollvalues = this_subsystem.raw_futures_roll(instrument_code) annroll = divide_df_single_column(rawrollvalues, rolldiffs) annroll.columns = ['annualised_roll'] return annroll
def _norm_returns(system, instrument_code, this_stage): this_stage.log.msg("Calculating normalised prices for %s" % instrument_code, instrument_code=instrument_code) returnvol = this_stage.daily_returns_volatility( instrument_code).shift(1) dailyreturns = this_stage.daily_returns(instrument_code) norm_return = divide_df_single_column(dailyreturns, returnvol) norm_return.columns = ["norm_return"] return norm_return
def _get_price_volatility(system, instrument_code): if hasattr(system, "rawdata"): daily_perc_vol = system.rawdata.get_daily_percentage_volatility( instrument_code) else: price = system.data.get_instrument_price(instrument_code) price = price.resample("1B", how="last") return_vol = robust_vol_calc(price.diff()) daily_perc_vol = 100.0 * divide_df_single_column(return_vol, price) return daily_perc_vol
def _get_price_volatility(system, instrument_code, this_stage_notused): if hasattr(system, "rawdata"): daily_perc_vol = system.rawdata.get_daily_percentage_volatility( instrument_code) else: price = system.data.daily_prices(instrument_code) return_vol = robust_vol_calc(price.diff()) daily_perc_vol = 100.0 * \ divide_df_single_column(return_vol, price) return daily_perc_vol
def ewmac(price, vol, Lfast, Lslow): """ Calculate the ewmac trading fule forecast, given a price and EWMA speeds Lfast, Lslow and vol_lookback Assumes that 'price' is daily data This version recalculates the price volatility, and does not do capping or scaling :param price: The price or other series to use (assumed Tx1) :type price: pd.DataFrame :param vol: The daily price unit volatility (NOT % vol) :type vol: pd.DataFrame (assumed Tx1) :param Lfast: Lookback for fast in days :type Lfast: int :param Lslow: Lookback for slow in days :type Lslow: int :returns: pd.DataFrame -- unscaled, uncapped forecast >>> from systems.provided.example.testdata import get_test_object_futures >>> from systems.basesystem import System >>> (rawdata, data, config)=get_test_object_futures() >>> system=System( [rawdata], data, config) >>> >>> ewmac(rawdata.get_instrument_price("EDOLLAR"), rawdata.daily_returns_volatility("EDOLLAR"), 64, 256).tail(2) price 2015-04-21 6.623348 2015-04-22 6.468900 """ # price: This is the stitched price series # We can't use the price of the contract we're trading, or the volatility will be jumpy # And we'll miss out on the rolldown. See # http://qoppac.blogspot.co.uk/2015/05/systems-building-futures-rolling.html # We don't need to calculate the decay parameter, just use the span # directly fast_ewma = pd.ewma(price, span=Lfast) slow_ewma = pd.ewma(price, span=Lslow) raw_ewmac = fast_ewma - slow_ewma return divide_df_single_column(raw_ewmac, vol, ffill=(False, True))
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 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(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 __init__(self, price, percentage=False, cost_per_block=None, SR_cost=None, capital=None, ann_risk_target=None, **kwargs): """ Create an account curve; from which many lovely statistics can be gathered We create by passing **kwargs which will be used by the pandl function :param percentage: Return % returns, or base currency if False :type percentage: bool :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 Note if both are included then cost_per_block will be disregarded :param capital: Capital at risk. Used for % returns, and calculating daily risk for SR costs :type capital: float or Tx1 :param ann_risk_target: Annual risk target, as % of capital. Used to calculate daily risk for SR costs :type ann_risk_target: float **kwargs passed to profit and loss calculation (price, trades, marktomarket, positions, delayfill, roundpositions, get_daily_returns_volatility, forecast, fx, value_of_price_point) """ returns_data=pandl_with_data(price, capital=capital, ann_risk_target=ann_risk_target, **kwargs) (cum_trades, trades, instr_ccy_returns, base_ccy_returns, fx)=returns_data gross_returns=base_ccy_returns ## always returns a time series (capital, daily_capital)=resolve_capital(gross_returns, capital, ann_risk_target) (costs_base_ccy, costs_instr_ccy)=calc_costs(returns_data, cost_per_block, SR_cost, daily_capital) net_returns=add_df_single_column(gross_returns,costs_base_ccy) ## costs are negative returns perc_gross_returns = divide_df_single_column( gross_returns, capital) perc_costs=divide_df_single_column( costs_base_ccy, capital) perc_net_returns=add_df_single_column(perc_gross_returns, perc_costs) if percentage: super().__init__(perc_gross_returns, perc_net_returns, perc_costs) else: super().__init__(gross_returns, net_returns, costs_base_ccy) setattr(self, "cum_trades", cum_trades) setattr(self, "trades", trades) setattr(self, "instr_ccy_returns", instr_ccy_returns) setattr(self, "base_ccy_returns", base_ccy_returns) setattr(self, "fx", fx) setattr(self, "capital", capital) setattr(self, "daily_capital", daily_capital) setattr(self, "costs_instr_ccy", costs_instr_ccy) setattr(self, "costs_base_ccy", costs_base_ccy) setattr(self, "ccy_returns", accountCurveSingle(gross_returns, net_returns, costs_base_ccy)) setattr(self, "perc_returns", accountCurveSingle(perc_gross_returns, perc_net_returns, perc_costs))
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