def lazy_property(fct, name=None):
    name = name or fct.__name__
    attr_name = '_' + name
    if attr_name == '_<lambda>':
        raise Exception("cannot assign <lambda> to lazy property")

    @property
    @wraps(fct)
    def _wrapper(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fct(self))
        return getattr(self, attr_name)

    return _wrapper class TxnPlColumns(object):
    DT = 'date'
    PID = TxnColumns.PID
    TID = TxnColumns.TID
    POS = 'pos'
    TXN_QTY = 'txn_qty'
    TXN_PX = 'txn_px'
    TXN_FEES = 'txn_fees'
    TXN_PREMIUM = 'txn_premium'
    TXN_INTENT = 'txn_intent'
    TXN_ACTION = 'txn_action'
    CLOSE_PX = 'close'
    OPEN_VAL = 'open_val'
    MKT_VAL = 'mkt_val'
    TOT_VAL = 'total_val'
    DVDS = 'dvds'
    FEES = 'fees'
    RPL_GROSS = 'rpl_gross'
    RPL = 'rpl'
    UPL = 'upl'
    PL = 'pl'

    DESCRIPTIONS = {
        DT: 'p/l date',
        POS: 'end of day position quantity',
        CLOSE_PX: 'end of day closing price',
        OPEN_VAL: 'open value of the position',
        MKT_VAL: 'market value',
        TOT_VAL: 'total of trade premiums',
        DVDS: 'dividends',
        FEES: 'fees',
        RPL_GROSS: 'realized gross p/l (TOT_VAL - OPEN_VAL)',
        RPL: 'realized pl (RPL_GROSS + FEES + DVDS)',
        UPL: 'unrealized pl (MKT_VAL + OPEN_VAL)',
        PL: 'Total p/l (UPL + RPL)'
    } @per_series(result_is_frame=1)
def drawdown_info(returns, geometric=True):
    """Return a DataFrame containing information about ALL the drawdowns for the rets. The frame
    contains the columns:
    'dd start': drawdown start date
    'dd end': drawdown end date
    'maxdd': maximium drawdown
    'maxdd dt': maximum drawdown
    'days': duration of drawdown
    """
    dd = drawdowns(returns, geometric=True).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()
    idxname = dd.index.name or 'index'
    ixs = dd.reset_index().groupby(['nonzero', 'gid'])[idxname].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 def drawdowns(returns, geometric=True):
    """
    compute the drawdown series for the period return series
    return: periodic return Series or DataFrame
    """
    wealth = 1. + returns_cumulative(returns, geometric=geometric, expanding=True)
    values = wealth.values
    if values.ndim == 2:
        ncols = values.shape[-1]
        values = np.vstack(([1.] * ncols, values))
        maxwealth = pd.expanding_max(values)[1:]
        dds = wealth / maxwealth - 1.
        dds[dds > 0] = 0  # Can happen if first returns are positive
        return dds
    elif values.ndim == 1:
        values = np.hstack(([1.], values))
        maxwealth = pd.expanding_max(values)[1:]
        dds = wealth / maxwealth - 1.
        dds[dds > 0] = 0  # Can happen if first returns are positive
        return dds
    else:
        raise ValueError('unable to process array with %s dimensions' % values.ndim) def guess_freq(index):
    # admittedly weak way of doing this...This needs to be abolished
    if isinstance(index, (pd.Series, pd.DataFrame)):
        index = index.index

    if hasattr(index, 'freqstr') and index.freqstr:
        return index.freqstr[0]
    elif len(index) < 3:
        raise Exception('cannot guess frequency with less than 3 items')
    else:
        lb = min(7, len(index))
        idx_zip = lambda: zip(index[-lb:-1], index[-(lb-1):])

        diff = min([t2 - t1 for t1, t2, in idx_zip()])
        if diff.days <= 1:
            if 5 in index.dayofweek or 6 in index.dayofweek:
                return 'D'
            else:
                return 'B'
        elif diff.days == 7:
            return 'W'
        else:
            diff = min([t2.month - t1.month for t1, t2, in idx_zip()])
            if diff == 1:
                return 'M'
            diff = min([t2.year - t1.year for t1, t2, in idx_zip()])
            if diff == 1:
                return 'A'

            strs = ','.join([i.strftime('%Y-%m-%d') for i in index[-lb:]])
            raise Exception('unable to determine frequency, last %s dates %s' % (lb, strs)) def downside_deviation(rets, mar=0, expanding=0, full=0, ann=0):
    """Compute the downside deviation for the specifed return series
    :param rets: periodic return series
    :param mar: minimum acceptable rate of return (MAR)
    :param full: If True, use the lenght of full series. If False, use only values below MAR
    :param expanding:
    :param ann: True if result should be annualized
    """
    below = rets[rets < mar]
    if expanding:
        n = pd.expanding_count(rets)[below.index] if full else pd.expanding_count(below)
        dd = np.sqrt(((below - mar) ** 2).cumsum() / n)
        if ann:
            dd *= np.sqrt(periods_in_year(rets))
        return dd.reindex(rets.index).ffill()
    else:
        n = rets.count() if full else below.count()
        dd = np.sqrt(((below - mar) ** 2).sum() / n)
        if ann:
            dd *= np.sqrt(periods_in_year(rets))
        return dd def periodicity(freq_or_frame):
    """
    resolve the number of periods per year
    """
    if hasattr(freq_or_frame, 'rule_code'):
        rc = freq_or_frame.rule_code
        rc = rc.split('-')[0]
        factor = PER_YEAR_MAP.get(rc, None)
        if factor is not None:
            return factor / abs(freq_or_frame.n)
        else:
            raise Exception('Failed to determine periodicity. No factor mapping for %s' % freq_or_frame)
    elif isinstance(freq_or_frame, basestring):
        factor = PER_YEAR_MAP.get(freq_or_frame, None)
        if factor is not None:
            return factor
        else:
            raise Exception('Failed to determine periodicity. No factor mapping for %s' % freq_or_frame)
    elif isinstance(freq_or_frame, (pd.Series, pd.DataFrame, pd.TimeSeries)):
        freq = freq_or_frame.index.freq
        if not freq:
            freq = pd.infer_freq(freq_or_frame.index)
            if freq:
                return periodicity(freq)
            else:
                # Attempt to resolve it
                import warnings

                freq = guess_freq(freq_or_frame.index)
                warnings.warn('frequency not set. guessed it to be %s' % freq)
                return periodicity(freq)
        else:
            return periodicity(freq)
    else:
        raise ValueError("periodicity expects DataFrame, Series, or rule_code property") def plot_return_on_dollar(rets, title='Return on $1', show_maxdd=0, figsize=None, ax=None, append=0, label=None, **plot_args):
    """ Show the cumulative return of specified rets and max drawdowns if selected."""
    crets = (1. + returns_cumulative(rets, expanding=1))
    if isinstance(crets, pd.DataFrame):
        tmp = crets.copy()
        for c in tmp.columns:
            s = tmp[c]
            fv = s.first_valid_index()
            fi = s.index.get_loc(fv)
            if fi != 0:
                tmp.ix[fi - 1, c] = 1.
            else:
                if not s.index.freq:
                    # no frequency set
                    freq = guess_freq(s.index)
                    s = s.asfreq(freq)
                first = s.index.shift(-1)[0]
                tmp = pd.concat([pd.DataFrame({c: [1.]}, index=[first]), tmp])
        crets = tmp
        if append:
            toadd = crets.index.shift(1)[-1]
            crets = pd.concat([crets, pd.DataFrame(np.nan, columns=crets.columns, index=[toadd])])
    else:
        fv = crets.first_valid_index()
        fi = crets.index.get_loc(fv)
        if fi != 0:
            crets = crets.copy()
            crets.iloc[fi - 1] = 1.
        else:
            if not crets.index.freq:
                first = crets.asfreq(guess_freq(crets.index)).index.shift(-1)[0]
            else:
                first = crets.index.shift(-1)[0]
            tmp = pd.Series([1.], index=[first])
            tmp = tmp.append(crets)
            crets = tmp

        if append:
            toadd = pd.Series(np.nan, index=[crets.index.shift(1)[-1]])
            crets = crets.append(toadd)

    ax = crets.plot(figsize=figsize, title=title, ax=ax, label=label, **plot_args)
    AxesFormat().Y.apply_format(new_float_formatter()).X.label("").apply(ax)
    #ax.tick_params(labelsize=14)
    if show_maxdd:
        # find the max drawdown available by using original rets
        if isinstance(rets, pd.DataFrame):
            iterator = rets.iteritems()
        else:
            iterator = iter([('', rets)])

        for c, col in iterator:
            dd, dt = max_drawdown(col, inc_date=1)
            lbl = c and c + ' maxdd' or 'maxdd'
            # get cret to place annotation correctly
            if isinstance(crets, pd.DataFrame):
                amt = crets.ix[dt, c]
            else:
                amt = crets[dt]

            bbox_props = dict(boxstyle="round", fc="w", ec="0.5", alpha=0.7)
            # sub = lambda c: c and len(c) > 2 and c[:2] or c
            try:
                dtstr = '{0}'.format(dt.to_period())
            except:
                dtstr = '{0}'.format(dt)

            ax.text(dt, amt, "mdd {0}".format(dtstr).strip(), ha="center",
                    va="center", size=10, bbox=bbox_props)
    plt.tight_layout() class AxesFormat(DeferredExecutionMixin):
    def __init__(self):
        super(AxesFormat, self).__init__()
        self.X = _XAxisFormat(self)
        self.Y = _YAxisFormat(self)
        self.axes = None

    def apply(self, axes=None):
        self.axes = axes or plt.gca()
        self.X()
        self.Y()
        self()

    def tight_layout(self, pad=1.08, h_pad=None, w_pad=None, rect=None):
        plt.tight_layout(pad, h_pad, w_pad, rect)
        return self def is_datetime_arraylike(arr):
    def __init__(self, fmtstr, coerce=True):
    def __call__(self, value):
    def __init__(self, precision=2, commas=True, parens=True, suffix=None, kind='f', coerce=True,
                 transform=None, nan='nan', prefix=None, lpad_zero=1, do_raise=0, trunc_dot_zeros=0):
    def __call__(self, value, **kwargs):
def new_int_formatter(commas=True, parens=True, prefix=None, suffix=None, coerce=True, nan='nan', trunc_dot_zeros=0):
def new_float_formatter(precision=2, commas=True, parens=True, prefix=None, suffix=None, coerce=True, nan='nan',
                        trunc_dot_zeros=0):
def new_thousands_formatter(precision=1, commas=True, parens=True, nan='nan', prefix=None, trunc_dot_zeros=0,
                            suffix='k'):
def new_millions_formatter(precision=1, commas=True, parens=True, nan='nan', prefix=None, trunc_dot_zeros=0,
                           suffix='M'):
def new_billions_formatter(precision=1, commas=True, parens=True, nan='nan', prefix=None, trunc_dot_zeros=0,
                           suffix='B'):
def new_trillions_formatter(precision=1, commas=True, parens=True, nan='nan', prefix=None, trunc_dot_zeros=0):
def new_percent_formatter(precision=2, commas=True, parens=True, prefix=None, suffix=None, coerce=True,
                          transform=lambda v: v,
                          nan='nan', trunc_dot_zeros=0):
def new_datetime_formatter(fmtstr='%d-%b-%y', coerce=True):
def guess_formatter(values, precision=1, commas=True, parens=True, nan='nan', prefix=None, pcts=0,
                    trunc_dot_zeros=0):
    def __init__(self, method=None, **formatter_args):
    def __call__(self, value, **kwargs):
def new_dynamic_formatter(method=None, precision=1, commas=True, parens=True, nan='nan', prefix=None, pcts=0,
                          trunc_dot_zeros=0):
class DateTimeFormat(object):
class NumberFormat(object):
class DynamicNumberFormat(object):
from collections import OrderedDict
from tia.util.decorator import lazy_property
from tia.analysis.model.interface import TxnPlColumns as TPL
from tia.analysis.perf import drawdown_info, drawdowns, guess_freq, downside_deviation, periodicity
from tia.analysis.plots import plot_return_on_dollar
from tia.util.mplot import AxesFormat
from tia.util.fmt import PercentFormatter, new_percent_formatter, new_float_formatter
        from tia.util.fmt import new_dynamic_formatter
import pandas as pd
import numpy as np
        txnrets.index = txnpl[TPL.DT]
        crets = CumulativeRets(txnrets)
        return Performance(crets)


class FixedAumRetCalculator(RetCalculator):
    def __init__(self, aum, reset_freq='M'):
        self.aum = aum
        self.reset_freq = reset_freq
        # capture what cash flows would be needed on reset date to reset the aum
        self.external_cash_flows = None

    def compute(self, txns):
        ltd = txns.pl.ltd_txn
        grouper = pd.TimeGrouper(self.reset_freq)
        period_rets = pd.Series(np.nan, index=ltd.index)
        aum = self.aum
        at = 0
        cf = OrderedDict()
        for key, grp in ltd.groupby(grouper):
            if grp.empty:
                continue
            eod = aum + grp
            sod = eod.shift(1)
            sod.iloc[0] = aum
            period_rets.iloc[at:at + len(grp.index)] = eod / sod - 1.
            at += len(grp.index)
            # get aum back to fixed amount
            cf[key] = eod.iloc[-1] - aum
        self.external_cash_flows = pd.Series(cf)
        crets = CumulativeRets(period_rets)
        return Performance(crets)


class AumRetCalculator(RetCalculator):
    def __init__(self, starting_aum, freq='M'):
        self.starting_aum = starting_aum
        self.freq = freq
        self.txn_aum = None

    def compute(self, txns):
        ltd = txns.pl.ltd_txn
        grouper = pd.TimeGrouper(self.freq)
        period_rets = pd.Series(np.nan, index=ltd.index)
        self.txn_aum = txn_aum = pd.Series(np.nan, index=ltd.index)
        sop = self.starting_aum
        at = 0
        for key, grp in ltd.groupby(grouper):
            if grp.empty:
                continue
            eod = sop + grp
            sod = eod.shift(1)
            sod.iloc[0] = sop
            period_rets.iloc[at:at + len(grp.index)] = eod / sod - 1.
            txn_aum.iloc[at:at + len(grp.index)] = sod
            at += len(grp.index)
            sop = eod.iloc[-1]
        crets = CumulativeRets(period_rets)
        return Performance(crets)


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')