Example #1
0
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)
Example #2
0
File: port.py Project: x829901/tia
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)
Example #3
0
File: ret.py Project: xie3ge/tia
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
Example #4
0
File: ret.py Project: xie3ge/tia
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)
Example #5
0
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
Example #6
0
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)
Example #7
0
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
Example #8
0
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