class TxnProfitAndLoss(ProfitAndLoss): def __init__(self, txns=None, txnpl_details=None): if txns is None and txnpl_details is None: raise ValueError('txns or txn_details must be specified') self.txns = txns self._txn_details = txnpl_details # Don't set the attribute, wany lazy property to be called #ProfitAndLoss.__init__(self, None) @property def txn_details(self): if self._txn_details is None: self._txn_details = TxnProfitAndLossDetails(self.txns) return self._txn_details txn_frame = property(lambda self: self.txn_details.frame) ltd_txn_frame = property(lambda self: self.txn_details.ltd_frame) txn = property(lambda self: self.txn_frame.set_index(PL.DT).pl) ltd_txn = property(lambda self: self.ltd_txn_frame.set_index(PL.DT).pl) dly_details = lazy_property(lambda self: self.txn_details.dly, 'dly_details') def truncate(self, before=None, after=None, pid=None): if before is None and after is None and pid is None: return self else: details = self.txn_details.truncate(before, after, pid) return TxnProfitAndLoss(txnpl_details=details) def get_pid_mask(self, pid): return self.txn_details.get_pid_mask(pid)
class SingleAssetPortfolio(object): def __init__(self, pricer, trades, ret_calc=None): """ :param pricer: PortfolioPricer :param trades: list of Trade objects """ self.trades = tuple(trades) self.pricer = pricer self._ret_calc = ret_calc or RoiiRetCalculator() txns = lazy_property(lambda self: Txns(self.trades, self.pricer, self.ret_calc), 'txns') positions = lazy_property(lambda self: Positions(self.txns), 'positions') pl = property(lambda self: self.txns.pl) performance = property(lambda self: self.txns.performance) # -------------------------------------------------- # direct access to common attributes dly_pl = property(lambda self: self.pl.dly) monthly_pl = property(lambda self: self.pl.monthly) dly_rets = property(lambda self: self.performance.dly) monthly_rets = property(lambda self: self.performance.monthly) @property def ret_calc(self): return self._ret_calc @ret_calc.setter def ret_calc(self, calc): self._ret_calc = calc if hasattr(self, '_txns'): self.txns.ret_calc = calc def clear_cache(self): for attr in ['_txns', '_positions', '_long', '_short']: if hasattr(self, attr): delattr(self, attr) def subset(self, pids): txns = self.txns stxns = txns.subset(pids) if stxns == txns: # return same object return self else: # TODO: rethink logic - maybe split trades (l/s) in Portfolio constructor as now # passing split trades back to portfolio subset port = SingleAssetPortfolio(self.pricer, stxns.trades, ret_calc=self.ret_calc) port._txns = stxns if hasattr(self, '_positions'): port._positions = self.positions.subset(stxns) return port @lazy_property def long(self): return PortfolioSubset.longs(self) @lazy_property def short(self): return PortfolioSubset.shorts(self) winner = property(lambda self: PortfolioSubset.winners(self)) loser = property(lambda self: PortfolioSubset.losers(self)) def buy_and_hold(self, qty=1., start_dt=None, end_dt=None, start_px=None, end_px=None): """Construct a portfolio which opens a position with size qty at start (or first data in pricer) and continues to the specified end date. It uses the end of day market prices defined by the pricer (or prices supplied) :param qty: :param start: datetime :param end: datetime :param which: which price series to use for inital trade px :param ret_cacls: portfolio return calculator :return: SingleAssetPortfolio """ from tia.analysis.model.trd import TradeBlotter eod = self.pricer.get_eod_frame().close start_dt = start_dt and pd.to_datetime(start_dt) or eod.index[0] start_px = start_px or eod.asof(start_dt) end_dt = end_dt and pd.to_datetime(end_dt) or eod.index[-1] end_px = end_px or eod.asof(end_dt) pricer = self.pricer.trunace(start_dt, end_dt) blotter = TradeBlotter() blotter.ts = start_dt blotter.open(qty, start_px) blotter.ts = end_dt blotter.close(end_px) trds = blotter.trades return SingleAssetPortfolio(pricer, trds, ret_calc=self.ret_calc)
class Performance(object): def __init__(self, txn_rets): if isinstance(txn_rets, pd.Series): txn_rets = CumulativeRets(txn_rets) self.txn_details = txn_rets txn = property(lambda self: self.txn_details.rets) ltd_txn = property(lambda self: self.txn_details.ltd_rets) dly_details = lazy_property(lambda self: self.txn_details.dly, 'dly_details') dly = property(lambda self: self.dly_details.rets) ltd_dly = property(lambda self: self.dly_details.ltd_rets) ltd_dly_ann = property(lambda self: self.dly_details.ltd_rets_ann) weekly_details = lazy_property(lambda self: self.txn_details.weekly, 'weekly_details') weekly = property(lambda self: self.weekly_details.rets) ltd_weekly = property(lambda self: self.weekly_details.ltd_rets) ltd_weekly_ann = property(lambda self: self.weekly_details.ltd_rets_ann) monthly_details = lazy_property(lambda self: self.txn_details.monthly, 'monthly_details') monthly = property(lambda self: self.monthly_details.rets) ltd_monthly = property(lambda self: self.monthly_details.ltd_rets) ltd_monthly_ann = property(lambda self: self.monthly_details.ltd_rets_ann) quarterly_details = lazy_property(lambda self: self.txn_details.quarterly, 'quarterly_details') quarterly = property(lambda self: self.quarterly_details.rets) ltd_quarterly = property(lambda self: self.quarterly_details.ltd_rets) ltd_quarterly_ann = property(lambda self: self.quarterly_details.ltd_rets_ann) annual_details = lazy_property(lambda self: self.txn_details.annual, 'annual_details') annual = property(lambda self: self.annual_details.rets) ltd_annual = property(lambda self: self.annual_details.ltd_rets) ltd_annual_ann = property(lambda self: self.annual_details.ltd_rets_ann) def iter_by_year(self): """Split the return objects by year and iterate""" for yr, details in self.txn_details.iter_by_year(): yield yr, Performance(details) def filter(self, txn_mask): details = self.txn_details.filter(txn_mask) return Performance(details) def truncate(self, before=None, after=None): details = self.txn_details.truncate(before, after) return Performance(details) def report_by_year(self, summary_fct=None, years=None, ltd=1, prior_n_yrs=None, first_n_yrs=None, ranges=None, bm_rets=None): """Summary the returns :param summary_fct: function(Rets) and returns a dict or Series :param years: int, array, boolean or None. If boolean and False, then show no years. If int or array show only those years, else show all years if None :param ltd: include live to date summary :param prior_n_years: integer or list. Include summary for N years of return data prior to end date :param first_n_years: integer or list. Include summary for N years of return data after start date :param ranges: list of ranges. The range consists of a year start and year end :param dm_dly_rets: daily return series for the benchmark for beta/alpha calcs :return: DataFrame """ if years and np.isscalar(years): years = [years] if summary_fct is None: def summary_fct(performance): monthly = performance.monthly_details dly = performance.dly_details data = OrderedDict() data['ltd ann'] = monthly.ltd_ann data['mret avg'] = monthly.mean data['mret std ann'] = monthly.std_ann data['sharpe ann'] = monthly.sharpe_ann data['sortino'] = monthly.sortino data['maxdd'] = dly.maxdd data['maxdd dt'] = dly.maxdd_dt if bm_rets is not None: abseries = performance.get_alpha_beta(bm_rets) prefix = {'weekly': 'wkly ', 'monthly': 'mret '}.get(abseries.name, abseries.name) data['{0}beta'.format(prefix)] = abseries['beta'] data['{0}alpha'.format(prefix)] = abseries['alpha'] data['avg dd'] = dly.dd_avg data['best month'] = monthly.rets.max() data['worst month'] = monthly.rets.min() data['nmonths'] = monthly.cnt return data results = OrderedDict() if years is not False: for yr, robj in self.iter_by_year(): if years is None or yr in years: results[yr] = summary_fct(robj) # First n years if first_n_yrs: first_n_yrs = first_n_yrs if not np.isscalar(first_n_yrs) else [first_n_yrs] for first in first_n_yrs: after = '12/31/%s' % (self.dly.index[0].year + first) firstN = self.truncate(after=after) results['first {0}yrs'.format(first)] = summary_fct(firstN) # Ranges if ranges: for range in ranges: yr_start, yr_end = range rng_rets = self.truncate('1/1/%s' % yr_start, '12/31/%s' % yr_end) results['{0}-{1}'.format(yr_start, yr_end)] = summary_fct(rng_rets) # Prior n years if prior_n_yrs: prior_n_yrs = prior_n_yrs if not np.isscalar(prior_n_yrs) else [prior_n_yrs] for prior in prior_n_yrs: before = '1/1/%s' % (self.dly.index[-1].year - prior) priorN = self.truncate(before) results['past {0}yrs'.format(prior)] = summary_fct(priorN) # LTD if ltd: results['ltd'] = summary_fct(self) return pd.DataFrame(results, index=results.values()[0].keys()).T
class CumulativeRets(object): def __init__(self, rets=None, ltd_rets=None): if rets is None and ltd_rets is None: raise ValueError('rets or ltd_rets must be specified') if rets is None: if ltd_rets.empty: rets = ltd_rets else: rets = (1. + ltd_rets).pct_change() rets.iloc[0] = ltd_rets.iloc[0] if ltd_rets is None: if rets.empty: ltd_rets = rets else: ltd_rets = (1. + rets).cumprod() - 1. self.rets = rets self.ltd_rets = ltd_rets pds_per_year = property(lambda self: periodicity(self.rets)) def asfreq(self, freq): other_pds_per_year = periodicity(freq) if self.pds_per_year < other_pds_per_year: msg = 'Cannot downsample returns. Cannot convert from %s periods/year to %s' raise ValueError(msg % (self.pds_per_year, other_pds_per_year)) if freq == 'B': rets = (1. + self.rets).groupby(self.rets.index.date).apply(lambda s: s.prod()) - 1. # If you do not do this, it will be an object index rets.index = pd.DatetimeIndex([i for i in rets.index]) return CumulativeRets(rets) else: rets = (1. + self.rets).resample(freq, how='prod') - 1. return CumulativeRets(rets) # ----------------------------------------------------------- # Resampled data dly = lazy_property(lambda self: self.asfreq('B'), 'dly') weekly = lazy_property(lambda self: self.asfreq('W'), 'weekly') monthly = lazy_property(lambda self: self.asfreq('M'), 'monthly') quarterly = lazy_property(lambda self: self.asfreq('Q'), 'quarterly') annual = lazy_property(lambda self: self.asfreq('A'), 'annual') # ----------------------------------------------------------- # Basic Metrics @lazy_property def ltd_rets_ann(self): return (1. + self.ltd_rets) ** (self.pds_per_year / pd.expanding_count(self.rets)) - 1. cnt = property(lambda self: self.rets.notnull().astype(int).sum()) mean = lazy_property(lambda self: self.rets.mean(), 'avg') mean_ann = lazy_property(lambda self: self.mean * self.pds_per_year, 'avg_ann') ltd = lazy_property(lambda self: self.ltd_rets.iloc[-1], name='ltd') ltd_ann = lazy_property(lambda self: self.ltd_rets_ann.iloc[-1], name='ltd_ann') std = lazy_property(lambda self: self.rets.std(), 'std') std_ann = lazy_property(lambda self: self.std * np.sqrt(self.pds_per_year), 'std_ann') drawdown_info = lazy_property(lambda self: drawdown_info(self.rets), 'drawdown_info') drawdowns = lazy_property(lambda self: drawdowns(self.rets), 'drawdowns') maxdd = lazy_property(lambda self: self.drawdown_info['maxdd'].min(), 'maxdd') dd_avg = lazy_property(lambda self: self.drawdown_info['maxdd'].mean(), 'dd_avg') kurtosis = lazy_property(lambda self: self.rets.kurtosis(), 'kurtosis') skew = lazy_property(lambda self: self.rets.skew(), 'skew') sharpe_ann = lazy_property(lambda self: np.divide(self.ltd_ann, self.std_ann), 'sharpe_ann') downside_deviation = lazy_property(lambda self: downside_deviation(self.rets, mar=0, full=0, ann=1), 'downside_deviation') sortino = lazy_property(lambda self: self.ltd_ann / self.downside_deviation, 'sortino') @lazy_property def maxdd_dt(self): ddinfo = self.drawdown_info if ddinfo.empty: return None else: return self.drawdown_info['maxdd dt'].ix[self.drawdown_info['maxdd'].idxmin()] # ----------------------------------------------------------- # Expanding metrics expanding_mean = property(lambda self: pd.expanding_mean(self.rets), 'expanding_avg') expanding_mean_ann = property(lambda self: self.expanding_mean * self.pds_per_year, 'expanding_avg_ann') expanding_std = lazy_property(lambda self: pd.expanding_std(self.rets), 'expanding_std') expanding_std_ann = lazy_property(lambda self: self.expanding_std * np.sqrt(self.pds_per_year), 'expanding_std_ann') expanding_sharpe_ann = property(lambda self: np.divide(self.ltd_rets_ann, self.expanding_std_ann)) # ----------------------------------------------------------- # Rolling metrics rolling_mean = property(lambda self: pd.rolling_mean(self.rets), 'rolling_avg') rolling_mean_ann = property(lambda self: self.rolling_mean * self.pds_per_year, 'rolling_avg_ann') def rolling_ltd_rets(self, n): return pd.rolling_apply(self.rets, n, lambda s: (1. + s).prod() - 1.) def rolling_ltd_rets_ann(self, n): tot = self.rolling_ltd_rets(n) return tot ** (self.pds_per_year / n) def rolling_std(self, n): return pd.rolling_std(self.rets, n) def rolling_std_ann(self, n): return self.rolling_std(n) * np.sqrt(self.pds_per_year) def rolling_sharpe_ann(self, n): return self.rolling_ltd_rets_ann(n) / self.rolling_std_ann(n) def iter_by_year(self): """Split the return objects by year and iterate""" for key, grp in self.rets.groupby(lambda x: x.year): yield key, CumulativeRets(rets=grp) def truncate(self, before=None, after=None): rets = self.rets.truncate(before=before, after=after) return CumulativeRets(rets=rets) @lazy_property def summary(self): d = OrderedDict() d['ltd'] = self.ltd d['ltd ann'] = self.ltd_ann d['mean'] = self.mean d['mean ann'] = self.mean_ann d['std'] = self.std d['std ann'] = self.std_ann d['sharpe ann'] = self.sharpe_ann d['sortino'] = self.sortino d['maxdd'] = self.maxdd d['maxdd dt'] = self.maxdd_dt d['dd avg'] = self.dd_avg d['cnt'] = self.cnt return pd.Series(d, name=self.rets.index.freq or guess_freq(self.rets.index)) def _repr_html_(self): from tia.util.fmt import new_dynamic_formatter fmt = new_dynamic_formatter(method='row', precision=2, pcts=1, trunc_dot_zeros=1, parens=1) df = self.summary.to_frame() return fmt(df)._repr_html_() def get_alpha_beta(self, bm_rets): if isinstance(bm_rets, pd.Series): bm = CumulativeRets(bm_rets) elif isinstance(bm_rets, CumulativeRets): bm = bm_rets else: raise ValueError('bm_rets must be series or CumulativeRetPerformace not %s' % (type(bm_rets))) bm_freq = guess_freq(bm_rets) if self.pds_per_year != bm.pds_per_year: tgt = {'B': 'dly', 'W': 'weekly', 'M': 'monthly', 'Q': 'quarterly', 'A': 'annual'}.get(bm_freq, None) if tgt is None: raise ValueError('No mapping for handling benchmark with frequency: %s' % bm_freq) tmp = getattr(self, tgt) y = tmp.rets y_ann = tmp.ltd_ann else: y = self.rets y_ann = self.ltd_ann x = bm.rets.truncate(y.index[0], y.index[-1]) x_ann = bm.ltd_ann model = pd.ols(x=x, y=y) beta = model.beta[0] alpha = y_ann - beta * x_ann return pd.Series({'alpha': alpha, 'beta': beta}, name=bm_freq) def plot_ltd(self, ax=None, style='k', label='ltd', show_dd=1, title=True, legend=1): ltd = self.ltd_rets ax = ltd.plot(ax=ax, style=style, label=label) if show_dd: dd = self.drawdowns dd.plot(style='r', label='drawdowns', alpha=.5, ax=ax) ax.fill_between(dd.index, 0, dd.values, facecolor='red', alpha=.25) fmt = PercentFormatter AxesFormat().Y.percent().X.label("").apply(ax) legend and ax.legend(loc='upper left', prop={'size': 12}) # show the actualy date and value mdt, mdd = self.maxdd_dt, self.maxdd bbox_props = dict(boxstyle="round", fc="w", ec="0.5", alpha=0.25) try: dtstr = '{0}'.format(mdt.to_period()) except: # assume daily dtstr = '{0}'.format(hasattr(mdt, 'date') and mdt.date() or mdt) ax.text(mdt, dd[mdt], "{1} \n {0}".format(fmt(mdd), dtstr).strip(), ha="center", va="top", size=8, bbox=bbox_props) if title is True: pf = new_percent_formatter(1, parens=False, trunc_dot_zeros=True) ff = new_float_formatter(precision=1, parens=False, trunc_dot_zeros=True) total = pf(self.ltd_ann) vol = pf(self.std_ann) sh = ff(self.sharpe_ann) mdd = pf(self.maxdd) title = 'ret$\mathregular{_{ann}}$ %s vol$\mathregular{_{ann}}$ %s sharpe %s maxdd %s' % ( total, vol, sh, mdd) title and ax.set_title(title, fontdict=dict(fontsize=10, fontweight='bold')) return ax def plot_ret_on_dollar(self, title=None, show_maxdd=1, figsize=None, ax=None, append=0, label=None, **plot_args): plot_return_on_dollar(self.rets, title=title, show_maxdd=show_maxdd, figsize=figsize, ax=ax, append=append, label=label, **plot_args) def plot_hist(self, ax=None, **histplot_kwargs): pf = new_percent_formatter(precision=1, parens=False, trunc_dot_zeros=1) ff = new_float_formatter(precision=1, parens=False, trunc_dot_zeros=1) ax = self.rets.hist(ax=ax, **histplot_kwargs) AxesFormat().X.percent(1).apply(ax) m, s, sk, ku = pf(self.mean), pf(self.std), ff(self.skew), ff(self.kurtosis) txt = '$\mathregular{\mu}$=%s $\mathregular{\sigma}$=%s skew=%s kurt=%s' % (m, s, sk, ku) bbox = dict(facecolor='white', alpha=0.5) ax.text(0, 1, txt, fontdict={'fontweight': 'bold'}, bbox=bbox, ha='left', va='top', transform=ax.transAxes) return ax def filter(self, mask, keep_ltd=0): if isinstance(mask, pd.Series): mask = mask.values rets = self.rets.ix[mask] ltd = None if keep_ltd: ltd = self.ltd_rets.ix[mask] return CumulativeRets(rets=rets, ltd_rets=ltd)
class ProfitAndLoss(object): def __init__(self, dly_details): self._dly_details = dly_details dly_details = property(lambda self: self._dly_details) dly_frame = property(lambda self: self.dly_details.frame) ltd_dly_frame = property(lambda self: self.dly_details.ltd_frame) dly = property(lambda self: self.dly_frame.pl) ltd_dly = property(lambda self: self.ltd_dly_frame.pl) weekly_details = lazy_property(lambda self: self.txn_details.weekly, 'weekly_details') weekly_frame = property(lambda self: self.weekly_details.frame) ltd_weekly_frame = property(lambda self: self.weekly_details.ltd_frame) weekly = property(lambda self: self.weekly_frame.pl) ltd_weekly = property(lambda self: self.ltd_weekly_frame.pl) monthly_details = lazy_property(lambda self: self.txn_details.monthly, 'monthly_details') monthly_frame = property(lambda self: self.monthly_details.frame) ltd_monthly_frame = property(lambda self: self.monthly_details.ltd_frame) monthly = property(lambda self: self.monthly_frame.pl) ltd_monthly = property(lambda self: self.ltd_monthly_frame.pl) quarterly_details = lazy_property(lambda self: self.txn_details.quarterly, 'quarterly_details') quarterly_frame = property(lambda self: self.quarterly_details.frame) ltd_quarterly_frame = property( lambda self: self.quarterly_details.ltd_frame) quarterly = property(lambda self: self.quarterly_frame.pl) ltd_quarterly = property(lambda self: self.ltd_quarterly_frame.pl) annual_details = lazy_property(lambda self: self.txn_details.annual, 'annual_details') annual_frame = property(lambda self: self.annual_details.frame) ltd_annual_frame = property(lambda self: self.annual_details.ltd_frame) annual = property(lambda self: self.annual_frame.pl) ltd_annual = property(lambda self: self.ltd_annual_frame.pl) def iter_by_year(self): for yr, details in self.dly_details.iter_by_year(): yield yr, ProfitAndLoss(details) def truncate(self, before=None, after=None, pid=None): if before is None and after is None and pid is None: return self else: details = self.dly_details.truncate(before, after) return ProfitAndLoss(details) def report_by_year(self, summary_fct=None, years=None, ltd=1, prior_n_yrs=None, first_n_yrs=None, ranges=None, bm_rets=None): """Summarize the profit and loss by year :param summary_fct: function(ProfitAndLoss) and returns a dict or Series :param years: int, array, boolean or None. If boolean and False, then show no years. If int or array show only those years, else show all years if None :param ltd: include live to date summary :param prior_n_years: integer or list. Include summary for N years of return data prior to end date :param first_n_years: integer or list. Include summary for N years of return data after start date :param ranges: list of ranges. The range consists of a year start and year end :param dm_dly_rets: daily return series for the benchmark for beta/alpha calcs :return: DataFrame """ if years and np.isscalar(years): years = [years] if summary_fct is None: def summary_fct(pl): monthly = pl.monthly_details dly = pl.dly_details data = OrderedDict() data['mpl avg'] = monthly.mean data['mpl std ann'] = monthly.std_ann data['maxdd'] = dly.maxdd data['maxdd dt'] = dly.maxdd_dt data['avg dd'] = dly.dd_avg data['best month'] = monthly.max data['worst month'] = monthly.min data['best day'] = dly.max data['worst day'] = dly.min data['nmonths'] = monthly.cnt return data results = OrderedDict() if years is not False: for yr, pandl in self.iter_by_year(): if years is None or yr in years: results[yr] = summary_fct(pandl) # First n years if first_n_yrs: first_n_yrs = first_n_yrs if not np.isscalar(first_n_yrs) else [ first_n_yrs ] for first in first_n_yrs: after = '12/31/%s' % (self.dly.index[0].year + first) firstN = self.truncate(after=after) results['first {0}yrs'.format(first)] = summary_fct(firstN) # Ranges if ranges: for range in ranges: yr_start, yr_end = range rng_rets = self.truncate('1/1/%s' % yr_start, '12/31/%s' % yr_end) results['{0}-{1}'.format(yr_start, yr_end)] = summary_fct(rng_rets) # Prior n years if prior_n_yrs: prior_n_yrs = prior_n_yrs if not np.isscalar(prior_n_yrs) else [ prior_n_yrs ] for prior in prior_n_yrs: before = '1/1/%s' % (self.dly.index[-1].year - prior) priorN = self.truncate(before) results['past {0}yrs'.format(prior)] = summary_fct(priorN) # LTD if ltd: results['ltd'] = summary_fct(self) return pd.DataFrame(results, index=list(results.values())[0].keys()).T
class ProfitAndLossDetails(object): def __init__(self, frame=None, ltd_frame=None): self._frame = frame self._ltd_frame = ltd_frame @property def ltd_frame(self): ltd = self._ltd_frame if ltd is None: if self._frame is None: raise Exception( 'Both frame and ltd frame are None. At least one must be defined.' ) self._ltd_frame = ltd = _dly_to_ltd(self._frame, PL.LTDS) return ltd @property def frame(self): obs = self._frame if obs is None: if self._ltd_frame is None: raise Exception( 'Both frame and ltd frames are None. At least one must be defined.' ) self._frame = obs = _ltd_to_dly(self._ltd_frame, PL.LTDS) return obs def rolling_frame(self, n): return pd.rolling_sum(self.frame, n) def asfreq(self, freq): """Resample the p&l at the specified frequency :param freq: :return: Pl object """ frame = self.frame if freq == 'B': resampled = frame.groupby( frame.index.date).apply(lambda f: f.sum()) resampled.index = pd.DatetimeIndex([i for i in resampled.index]) return ProfitAndLossDetails(resampled) else: resampled = frame.resample(freq, how='sum') return ProfitAndLossDetails(resampled) @lazy_property def drawdown_info(self): dd = self.drawdowns.to_frame() last = dd.index[-1] dd.columns = ['vals'] dd['nonzero'] = (dd.vals != 0).astype(int) dd['gid'] = (dd.nonzero.shift(1) != dd.nonzero).astype(int).cumsum() ixs = dd.reset_index().groupby( ['nonzero', 'gid'])[dd.index.name or 'index'].apply(lambda x: np.array(x)) rows = [] if 1 in ixs: for ix in ixs[1]: sub = dd.ix[ix] # need to get t+1 since actually draw down ends on the 0 value end = dd.index[dd.index.get_loc(sub.index[-1]) + (last != sub.index[-1] and 1 or 0)] rows.append( [sub.index[0], end, sub.vals.min(), sub.vals.idxmin()]) f = pd.DataFrame.from_records( rows, columns=['dd start', 'dd end', 'maxdd', 'maxdd dt']) f['days'] = (f['dd end'] - f['dd start']).astype('timedelta64[D]') return f @lazy_property def drawdowns(self): ltd = self.ltd_frame.pl maxpl = pd.expanding_max(ltd) maxpl[maxpl < 0] = 0 dd = ltd - maxpl return dd # scalar data cnt = property(lambda self: self.frame.pl.notnull().astype(int).sum()) mean = lazy_property(lambda self: self.frame.pl.mean(), 'mean') avg = mean std = lazy_property(lambda self: self.frame.pl.std(), 'std') std_ann = lazy_property( lambda self: np.sqrt(periods_in_year(self.frame.pl)) * self.std, 'std_ann') maxdd = lazy_property(lambda self: self.drawdown_info['maxdd'].min(), 'maxdd') dd_avg = lazy_property(lambda self: self.drawdown_info['maxdd'].mean(), 'dd_avg') min = property(lambda self: self.frame.pl.min()) max = property(lambda self: self.frame.pl.max()) @lazy_property def maxdd_dt(self): if self.drawdown_info.empty: return None else: return self.drawdown_info['maxdd dt'].ix[ self.drawdown_info['maxdd'].idxmin()] @lazy_property def summary(self): d = OrderedDict() d['avg'] = self.avg d['std'] = self.std d['maxdd'] = self.maxdd d['maxdd dt'] = self.maxdd_dt d['dd avg'] = self.dd_avg d['cnt'] = self.cnt return pd.Series(d, name=self.frame.index.freq or guess_freq(self.frame.index)) def _repr_html_(self): from tia.util.fmt import new_dynamic_formatter fmt = new_dynamic_formatter(method='row', precision=2, pcts=1, trunc_dot_zeros=1, parens=1) return fmt(self.summary.to_frame())._repr_html_() def plot_ltd(self, ax=None, style='k', label='ltd', show_dd=1, guess_xlabel=1, title=True): ltd = self.ltd_frame.pl ax = ltd.plot(ax=ax, style=style, label=label) if show_dd: dd = self.drawdowns dd.plot(style='r', label='drawdowns', alpha=.5) ax.fill_between(dd.index, 0, dd.values, facecolor='red', alpha=.25) fmt = lambda x: x # guess the formatter if guess_xlabel: from tia.util.fmt import guess_formatter from tia.util.mplot import AxesFormat fmt = guess_formatter(ltd.abs().max(), precision=1) AxesFormat().Y.apply_format(fmt).apply(ax) ax.legend(loc='upper left', prop={'size': 12}) # show the actualy date and value mdt, mdd = self.maxdd_dt, self.maxdd bbox_props = dict(boxstyle="round", fc="w", ec="0.5", alpha=0.25) try: dtstr = '{0}'.format(mdt.to_period()) except: # assume daily dtstr = '{0}'.format( hasattr(mdt, 'date') and mdt.date() or mdt) ax.text(mdt, dd[mdt], "{1} \n {0}".format(fmt(mdd), dtstr).strip(), ha="center", va="top", size=8, bbox=bbox_props) if title is True: df = new_dynamic_formatter(precision=1, parens=False, trunc_dot_zeros=True) total = df(ltd.iloc[-1]) vol = df(self.std) mdd = df(self.maxdd) title = 'pnl %s vol %s maxdd %s' % (total, vol, mdd) title and ax.set_title(title, fontdict=dict(fontsize=10, fontweight='bold')) return ax def truncate(self, before=None, after=None): if before is None and after is None: return self else: sub = self.frame.truncate(before, after) return ProfitAndLossDetails(frame=sub)
class TxnProfitAndLossDetails(object): def __init__(self, txns=None, frame=None, ltd_frame=None): """ :param txns: Txns object """ if txns is None and frame is None and ltd_frame is None: raise ValueError('Either {txns, frame, ltd_frame} must be defined') self.txns = txns self._frame = frame self._ltd_frame = ltd_frame self.ltd_cols = [ TPL.FEES, TPL.TOT_VAL, TPL.RPL_GROSS, TPL.DVDS, TPL.RPL, TPL.RPL, TPL.UPL, TPL.PL ] @property def ltd_frame(self): if self._ltd_frame is None: if self._frame is not None: self._ltd_frame = _dly_to_ltd(self._frame, self.ltd_cols) elif self.txns is not None: self._ltd_frame = OpenAverageProfitAndLossCalculator().compute( self.txns) else: raise Exception('either txns or pl frame must be defined') return self._ltd_frame @property def frame(self): if self._frame is None: ltd = self.ltd_frame self._frame = _ltd_to_dly(ltd, self.ltd_cols) return self._frame def asfreq(self, freq): frame = self.frame pl = frame[PL.ALL].set_index(PL.DT) if freq == 'B': resampled = pl.groupby(pl.index.date).apply(lambda f: f.sum()) resampled.index = pd.DatetimeIndex([i for i in resampled.index]) return ProfitAndLossDetails(resampled) else: resampled = pl.resample(freq, how='sum') return ProfitAndLossDetails(resampled) # ----------------------------------------------------------- # Resampled data dly = lazy_property(lambda self: self.asfreq('B'), 'dly') weekly = lazy_property(lambda self: self.asfreq('W'), 'weekly') monthly = lazy_property(lambda self: self.asfreq('M'), 'monthly') quarterly = lazy_property(lambda self: self.asfreq('Q'), 'quarterly') annual = lazy_property(lambda self: self.asfreq('A'), 'annual') def get_pid_mask(self, pid): return self.frame[TPL.PID] == pid def truncate(self, before=None, after=None, pid=None): if before is None and after is None and pid is None: return self elif before or after: sub = self.frame.truncate(before, after) return TxnProfitAndLossDetails(frame=sub) else: mask = self.get_pid_mask(pid) frame = self.frame sub = frame.ix[mask.values] return TxnProfitAndLossDetails(frame=sub) def iter_by_year(self): for key, grp in self.frame.groupby(self.frame[TPL.DT].dt.year): yield key, TxnProfitAndLossDetails(frame=grp) def subset(self, txns): """To perform a subset it is not possible to reuse the frame since it is LTD, so we convert to daily then compute ltd from daily :param txns: the update Txns object :return: """ result = TxnProfitAndLossDetails(txns) # TODO - add reusing calcs. Issue is when removing PIDs, then could be multiple entries per dt # use daily txn, clear all values where != pid # determine which Timestamp columns can be removed as an old position may have multiple txns on same day # recreate ltd from dly # Need to take care if a dvd occurs at end of day return result
class Txns(object): def __init__(self, trades, pricer, ret_calc=None): """ #TODO - rethink if user should split trades prior to calling this method... :param trades: list of Trade objects :param pricer: """ # split into l/s positions self.trades = tuple(iter_txns(trades)) self.pricer = pricer self._ret_calc = ret_calc or RoiiRetCalculator() pids = property(lambda self: self.frame[TC.PID].unique()) pl = lazy_property(lambda self: TxnProfitAndLoss(self), "pl") performance = lazy_property(lambda self: self.ret_calc.compute(self), "performance") @property def ret_calc(self): return self._ret_calc @ret_calc.setter def ret_calc(self, calc): self._ret_calc = calc if hasattr(self, "_performance"): delattr(self, "_performance") @lazy_property def frame(self): """Convert the trades to transaction level details necessary for long/short accouting. :param trades: :param pricer: provides the interface to get premium for a specified quanity, price, and timestamp. :return: """ rows = [] pricer = self.pricer pos = open_val = pid = 0 for txn in self.trades: # These values always get copied qty = txn.qty premium = pricer.get_premium(qty, txn.px, ts=txn.ts) if pos == 0: # Open position side = qty > 0 and Action.Buy or Action.SellShort open_val = premium pid += 1 side = side intent = Intent.Open pos = qty elif pos + qty == 0: # close position side = qty > 0 and Action.Cover or Action.Sell open_val = 0 side = side intent = Intent.Close pos = 0 elif is_increase(pos, qty): side = txn.qty > 0 and Action.Buy or Action.SellShort open_val += premium pos += qty intent = Intent.Increase side = side else: # decrease - no worries about split since iterator takes care of it side = txn.qty > 0 and Action.Cover or Action.Sell open_val *= (pos + qty) / pos pos += qty intent = Intent.Decrease side = side # Get rid of anything but the date dt = txn.ts.to_period("B").to_timestamp() rows.append([ dt, txn.ts, pid, txn.tid, txn.qty, txn.px, txn.fees, premium, open_val, pos, intent, side, ]) df = pd.DataFrame.from_records( rows, columns=[ TC.DT, TC.TS, TC.PID, TC.TID, TC.QTY, TC.PX, TC.FEES, TC.PREMIUM, TC.OPEN_VAL, TC.POS, TC.INTENT, TC.ACTION, ], ) df.index.name = "seq" return df def get_pid_txns(self, pid): pmask = self.frame[TC.PID] == pid assert len(pmask.index) == len( self.trades), "assume 1-1 ratio of trade to row in frame" return tuple(pd.Series(self.trades)[pmask.values]) def subset(self, pids): pmask = self.frame[TC.PID].isin(pids) if pmask.all(): return self else: # 1 to 1 mapping of txn to row (so can figure out trades from mask) trds = tuple(pd.Series(self.trades)[pmask.values]) # build the object result = Txns(trds, self.pricer, self.ret_calc) result._frame = self.frame.ix[pmask] if hasattr(self, "_profit_and_loss"): pl = self.profit_and_loss result._profit_and_loss = pl.subset(result) return result