Exemple #1
0
def investment_performance(s,
                           *,
                           alternate_investment=None,
                           signals=None,
                           dates=None):
    '''Return interesting facts about an investment in a :code:`Performance` object.'''

    dates = dates or core.current_dates()
    alternate_investment = alternate_investment or constant(1)
    signals = signals and to_signals(signals)

    with core.date_scope(dates):

        equity = equity_line(
            s, signals,
            alternate_investment=alternate_investment) if signals else s

        vol_res = 1 - volatility(equity)
        dd_res = series_drawdown(equity).residual()
        annualized = gpa(equity)

        return Performance(
            volatility_residual=vol_res,
            drawdown_residual=dd_res,
            annualized=annualized,
            long_ratio=(long_ratio(signals) if signals else None),
            signal_count=(signals and series_count(signals)))
Exemple #2
0
def conviction(s, N, dates=None):
    '''A conviction signal series avoids signal whipsaws by forcing signals to
    persist N periods before firing.  The signal, if fired, is delayed by N periods
    as well.  For instance, with a conviction of 1, two sequential signals are 
    simply erased.  If not erased, the signal values of the input are delayed by 
    one period.'''

    dates = dates or core.current_dates()
    dv = [dt for dt in dates]
    vals = list(map(s.f, dates))
    sigv = signalify_vector_copy(vals)

    last_ndx = -1000000
    for (ndx, sig) in enumerate(sigv):
        if sig:
            if (ndx - last_ndx <= N):
                sigv[ndx] = None
                sigv[last_ndx] = None
            last_ndx = ndx

    fd = dates.first_date()
    ld = dates.last_date()
    outv = [None for dt in range(fd, ld + 1)]

    # warp
    for (sig, dt) in zip(sigv, dv[N:]):
        if sig:
            outv[dt - fd] = sig

    return core.vector_series(outv, fd)
Exemple #3
0
def prepend(s, *, surrogate=None, dates=None):

    dates = (dates or core.current_dates())
    if surrogate is None:
        raise Exception("surrogate required")
    ob = first_ob(add(s, surrogate), dates=dates)
    if ob is None:
        raise Exception("no observation")

    base_f = s.f
    surr_f = surrogate.f
    common_date = ob[0]
    ratio = s.f(ob[0]) / surr_f(ob[0])

    dv = dates.vec
    fd = dates.first_date()
    ld = dates.last_date()
    outv = [None for dt in range(fd, ld + 1)]

    for (ndx, dt) in enumerate(dv):
        val = base_f(dt)
        if not core.is_valid_num(val) and dt < common_date:
            val = surr_f(dt) * ratio
        if core.is_valid_num(val):
            outv[dt - fd] = val

    return core.vector_series(outv, fd, name="prepend(...)")
Exemple #4
0
def repeated(s, repeat_last=False, dates=None):
    '''Fill missing values in a series with preceding ones, optionally
    continuing the last valid observation.'''

    dates = dates or core.current_dates()
    vals = list(map(s.f, dates))

    fd = dates.first_date()
    ld = dates.last_date()

    prev = vals[0]
    last_valid_date = fd
    for ndx, val in enumerate(vals):
        if ndx and not core.is_valid_num(val):
            vals[ndx] = prev
        else:
            last_valid_date = dates.vec[ndx]
        prev = vals[ndx]

    fd = dates.first_date()
    ld = dates.last_date()

    outv = [None for dt in range(fd, ld + 1)]

    for (dt, val) in zip(dates, vals):
        if (not repeat_last) and (dt > last_valid_date):
            break
        if core.is_valid_num(val):
            outv[dt - fd] = val

    return core.vector_series(outv, fd)
Exemple #5
0
def mo_days(s, days, dates=None):
    '''Similar to :code:`mo(s,N)`, :code:`mo_days(s,days)` calculates 
    the new/old ratios along the specified dates for each new, but 
    each old-date is based upon calendar days rather than periods.
    If the calendar days back does not line up with a market day, the
    value of most recent available earlier observation is selected.'''

    sf = s.f
    sf_old = (fudge(s)).f

    if days < 1:
        raise Exception('expected positive days')

    dates = dates or core.current_dates()
    dv = dates.vec
    fd = dv[0]
    outv = [None for dt in range(dv[0], dv[-1] + 1)]

    for dt in dates:
        r = ratio(sf_old(dt - days), sf(dt))
        if core.is_valid_num(r):
            outv[dt - fd] = r

    name = "mo_days({},{})".format(abbreviate(s), days)
    return core.vector_series(outv, fd, name=name)
Exemple #6
0
def ma(s, N, dates=None):
    '''Create running N-period arithmetic average of input series.'''

    dates = dates or core.current_dates()
    vals = series_dates_values(s, dates)
    fd = dates.first_date()
    ld = dates.last_date()
    outv = [None for dt in range(fd, ld + 1)]

    total = 0
    consecutive = 0

    for ndx, dt in enumerate(dates):
        val = vals[ndx]
        if core.is_valid_num(val):
            total += val
            consecutive += 1
        else:
            consecutive = 0
            total = 0
        if consecutive > N:
            total -= vals[ndx - N]
        if consecutive >= N:
            outv[dt - fd] = (total / N)

    return core.vector_series(outv, fd, name="SMA({})".format(N))
Exemple #7
0
def first_ob(s, dates=None):
    '''Return first available (dt,value) tuple.'''
    dates = dates or core.current_dates()
    f = s.f
    for dt in dates:
        val = f(dt)
        if core.is_valid_num(val):
            return (dt, val)
    return None
Exemple #8
0
def gpa(s, dates=None):
    '''Calculate the annualized gain of series s over dates.'''
    dates = dates or core.current_dates()
    early = first_ob(s, dates=dates)
    late = last_ob(s, dates=dates)
    days = late[0] - early[0]
    years = days / 365.2425
    gain = late[1] / early[1]
    return (gain**(1 / years)) - 1
Exemple #9
0
def series_count(s, dates=None):
    '''Returns the number of observations in a series.'''

    dates = dates or core.current_dates()

    def valid(dt):
        return core.is_valid_num(s.f(dt))

    return sum((1 for dt in dates if valid(dt)))
Exemple #10
0
def measure(name, thunk):
    ITERATIONS = 10
    start = time.time()
    for i  in range(ITERATIONS):
        v = thunk()
        f = v.f
        for dt in core.current_dates():
            f(dt)
    elapsed = time.time() - start
    print('{:12}\t{}'.format(name, int(ITERATIONS / 1.0 / elapsed)))
Exemple #11
0
def last_ob(s, dates=None):
    '''Return last available (dt,value) tuple.'''
    dates = dates or core.current_dates()
    dv = dates.vec
    f = s.f
    for ndx in range(len(dv) - 1, -1, -1):
        dt = dv[ndx]
        val = f(dt)
        if core.is_valid_num(val):
            return (dt, val)
    return None
Exemple #12
0
def to_signals(s, dates=None):
    '''Transform a series into non-repeating negative-one (-1) where the input is negative
       non-repeating one (1) where the input is non-negative.'''

    dates = dates or core.current_dates()
    vals = list(map(s.f, dates.vec))
    sigv = signalify_vector_copy(vals)

    fd = dates.first_date()
    ld = dates.last_date()
    outv = [None for dt in range(fd, ld + 1)]

    for (dt, sig) in zip(dates, sigv):
        if sig:
            outv[dt - fd] = sig

    return core.vector_series(outv, fd, name='sigs({})'.format(abbreviate(s)))
Exemple #13
0
def dump(*seriess, first=None, last=None, dts=None):
    '''dump takes multiple series, with optional dates constraints and
    lists them in date order.  It is only useful interactively, but in that
    case, very useful.'''
    def format(n):
        return '{:12.4f}'.format(n)

    sfs = [s.f for s in seriess]
    dts = dts or core.current_dates()
    fd = (first or dts.first_date())
    ld = (last or dts.last_date())
    for dt in range(fd, ld + 1):
        nums = [f(dt) for f in sfs]
        if sum([(1 if core.is_valid_num(n) else 0) for n in nums]):
            rounded = map(
                lambda n: (format(n)
                           if core.is_valid_num(n) else '            '), nums)
            print("{} {}".format(core.jdate_to_text(dt), ' '.join(rounded)))
Exemple #14
0
def series_drawdowns(s, *, max_residual=1.0, dates=None):
    '''series_drawdowns returns all non-overlapping drawdowns,
       constrained by max_residual.  The result is ordered
       by the residual, i.e. worst drawdown first.'''

    dates = dates or core.current_dates()
    if (len(dates.vec) < 2):
        return []
    dd = series_drawdown(s, dates=dates)
    if not dd or (dd.residual() > max_residual):
        return []
    dd_left = series_drawdowns(s,
                               max_residual=max_residual,
                               dates=core.dates(dates, last=dd.max[0])) or []
    dd_right = series_drawdowns(
        s, max_residual=max_residual, dates=core.dates(dates,
                                                       first=dd.min[0])) or []
    return sorted(dd_left + [dd] + dd_right, key=lambda a: a.residual())
Exemple #15
0
def cross(*,
          slower=None,
          faster=None,
          upfactor=1.0,
          downfactor=1.0,
          dates=None):
    '''cross generates a signal series, i.e. series of [1,-1,None], set to 1
    where the faster series moves above the slower series and -1 where it moves
    below. Changing the upfactor from its default 1.0 changes the border for 
    crossing.  For instance, upfactor=1.02 means that the faster series must 
    exceed the lower series by 2% before generating the buy value of 1.  Likewise,
    setting downfactor to .98 would require passing 2% below the faster series
    to generate a sell signal of -1.  As usual, dates can be supplied, but default
    to the current_dates() value.'''

    if (not slower) or (not faster):
        raise Exception('slower,faster are required keyword arguments')

    dates = dates or core.current_dates()
    sv = series_dates_values(slower, dates)
    fv = series_dates_values(faster, dates)

    obs = []
    prev_sig = None

    for ndx, dt in enumerate(dates):
        s_val = sv[ndx]
        f_val = fv[ndx]

        if not (core.is_valid_num(s_val) and core.is_valid_num(f_val)):
            continue

        if f_val > s_val * upfactor:
            sig = 1
        elif f_val < s_val * downfactor:
            sig = -1
        else:
            sig = None

        if sig and (sig != prev_sig):
            obs.append((dt, sig))
            prev_sig = sig

    return obs_to_series(obs)
Exemple #16
0
def warp(s, N, dates=None):
    '''warp creates an N-period shift of values within dateset.  Negative periods shift data
    backward in time.  Often a signal series if warped with N=1 to measure 
    performance of a trading scheme if trading happens the next market day.'''

    dates = (dates or core.current_dates())
    dv = dates.vec
    sf = s.f
    fd = dates.first_date()
    ld = dates.last_date()
    outv = [None for dt in range(fd, ld + 1)]

    for (ndx, dt) in enumerate(dv):
        val = sf(dt)
        if core.is_valid_num(val):
            new_ndx = ndx + N
            if 0 <= new_ndx < len(dv):
                new_dt = dv[new_ndx]
                outv[new_dt - fd] = val

    return core.vector_series(outv, fd, name="warp({})".format(N))
Exemple #17
0
def fractional(s, fraction, dates=None):
    '''A fractional smoothes a series such that the current value is weighted by some fraction 
    in (0..1) added to the previous value weighted by (1 - fraction).'''

    dates = dates or core.current_dates()
    fd, ld = s.first_date(), s.last_date()
    outv = [None for dt in range(fd, ld + 1)]
    remainder = 1 - fraction
    prev = None
    f = s.f
    for dt in dates:
        val = f(dt)
        newVal = ((fraction * val + remainder * prev) if
                  (core.is_valid_num(prev)
                   and core.is_valid_num(val)) else val)
        outv[dt - fd] = newVal
        prev = newVal
    return core.vector_series(outv,
                              fd,
                              name="fractional({},{})".format(
                                  abbreviate(s), fraction))
Exemple #18
0
def window_series (s, N, proc, dates=None, missing_data_permitted=False):

    dates = (dates or core.current_dates())
    dv = dates.vec
    sf = s.f
    fd = dates.first_date()
    ld = dates.last_date()
    vv = [(n if core.is_valid_num(n) else None) for n in (sf(dt) for dt in dv)]
    outv = [ None for dt in range(fd,ld+1) ]

    count = 0
    for (ndx, dt) in enumerate(dv):
        val = sf(dt)
        count = (count+1 if (missing_data_permitted or core.is_valid_num(val)) else 0)
        if count >= N:
            stop = ndx+1
            start = stop-N
            result = proc(vv[start:stop])
            if core.is_valid_num(result):
                outv[dt - fd] = result

    return core.vector_series(outv, fd, name="window_series({})".format(N))
Exemple #19
0
def mo(s, N, dates=None):
    '''Return the N-period ratio series of new/old values.'''

    if N < 1:
        raise Exception('requires period > 0')

    dates = dates or core.current_dates()
    sf = s.f
    dv = dates.vec
    shifted_dv = dv[min(N, len(dv)):]
    outv = [None for dt in range(dv[0], dv[-1] + 1)]
    fd = dv[0]

    for (early, late) in zip(dv, shifted_dv):
        e_val = sf(early)
        l_val = sf(late)
        r = ratio(e_val, l_val)
        if core.is_valid_num(r):
            outv[late - fd] = r

    name = "mo({},{})".format(abbreviate(s), N)
    return core.vector_series(outv, fd, name=name)
Exemple #20
0
def unrepeated(s, dates=None):
    '''Copy the input series, suppressing repeated values.'''

    dates = dates or core.current_dates()
    vals = list(map(s.f, dates))

    prev = vals[0]
    for ndx, val in enumerate(vals):
        if ndx and val == prev:
            vals[ndx] = None
        prev = val

    fd = dates.first_date()
    ld = dates.last_date()

    outv = [None for dt in range(fd, ld + 1)]

    for (dt, val) in zip(dates, vals):
        if core.is_valid_num(val):
            outv[dt - fd] = val

    return core.vector_series(outv, fd)
Exemple #21
0
def min_max_obs(s, dates=None):
    '''Return a 2-tuple of (dt,value) observations representing
    the points of minimum and maximum values respectively.'''

    dates = dates or core.current_dates()
    min_ob = None
    max_ob = None
    f = s.f

    for dt in dates:
        val = f(dt)
        if core.is_valid_num(val):
            if not min_ob:
                min_ob = max_ob = (dt, val)
            if min_ob[1] > val:
                min_ob = (dt, val)
            if max_ob[1] < val:
                max_ob = (dt, val)

    if not min_ob:
        raise Exception("no observations")

    return min_ob, max_ob
Exemple #22
0
def reversals(s, down_factor=1.0, up_factor=1.0, dates=None):
    '''When a series ascends above up-factor multiplied by a preceding 
    local minimum, a buy (1) signal is produced.  Upon 
    descending below the product of a local maximum and down-factor, 
    a sell (-1) signal is produced.
    '''
    dates = dates or core.current_dates()
    fd, ld = core.first_date(dates), core.last_date(dates)
    outv = [None for dt in range(fd, ld + 1)]
    min_ob = max_ob = first_ob(s, dates)
    sf = s.f
    state = None

    for dt in dates:

        val = sf(dt)
        if not core.is_valid_num(val):
            continue

        if val > max_ob[1]:
            max_ob = (dt, val)
        if val < min_ob[1]:
            min_ob = (dt, val)

        if (1 != state) and (val > min_ob[1] * up_factor):
            max_ob = min_ob = (dt, val)
            outv[dt - fd] = 1
            state = 1

        elif (-1 != state) and (val < max_ob[1] * down_factor):
            max_ob = min_ob = (dt, val)
            outv[dt - fd] = -1
            state = -1

    return core.vector_series(outv,
                              fd,
                              name="reversals({})".format(abbreviate(s)))
Exemple #23
0
def series_drawdown(s, dates=None):
    '''series_drawdown returns a drawdown object containing the
    beginning and ending observation points in an input series resulting
    in the greatest decrease in value.  This answers the question "How much
    unrealized loss did this investment have?"'''

    dates = dates or core.current_dates()
    obs = series_to_obs(dates, s)

    acc = []
    for ob in reversed(obs):
        if (not acc) or (ob[1] < acc[-1][1]):
            acc.append(ob)
    reversed_mins = list(reversed(acc))

    acc = []
    for ob in obs:
        if (not acc) or (ob[1] > acc[-1][1]):
            acc.append(ob)
    maxs = acc

    def find_following_min(mx_ob):
        for ob in reversed_mins:
            if ob[0] > mx_ob[0]:
                return ob
        return None

    def make_drawdown(mx_ob):
        mn_ob = find_following_min(mx_ob)
        return drawdown(mx_ob, mn_ob) if mn_ob else None

    def more_drawdown(a, b):
        return a if (a.residual() < b.residual()) else b

    pairs = list(filter(None, [make_drawdown(mx_ob) for mx_ob in maxs]))

    return functools.reduce(more_drawdown, pairs) if len(pairs) else None
Exemple #24
0
def capture(s, benchmark, period, dates, predicate):

    if not benchmark:
        raise Exception("benchmark series must be specified")
    if not period:
        raise Exception("expected period > 0")

    dates = (dates or core.current_dates())

    sf = mo(s, period).f
    bf = mo(benchmark, period).f

    B_total = 0
    S_total = 0

    for dt in dates.vec:
        B_val = bf(dt)
        if core.is_valid_num(B_val) and predicate(B_val):
            S_val = sf(dt)
            if core.is_valid_num(S_val):
                B_total = B_total + B_val
                S_total = S_total + S_val

    return (S_total / B_total) if B_total else None
Exemple #25
0
 def verify_two_series(self,a,b):
     f1 = a.f
     f2 = b.f
     for dt in core.current_dates():
         self.assertEqual(approx(f1(dt)),approx(f2(dt)))
Exemple #26
0
 def setUp(self):
     self.TEST_SERIES = obs_to_series(TEST_SERIES_OBS)
     core.current_dates(core.dates(self.TEST_SERIES))
Exemple #27
0
def volatility(s, days=365, dates=None):
    '''volatility is the standard deviation of the mo_days calculation.'''

    dates = dates or core.current_dates()
    vals = series_dates_values(mo_days(s, days=days, dates=dates), dates)
    return standard_deviation(vals)
Exemple #28
0
def equity_line(s,
                signals,
                initial_value=100,
                alternate_investment=None,
                dates=None):
    '''The point of most signal generation is to go in and out of invested position
    on some series, optionally buying an alternate investment such as short-duration debt.
    The equity_line series represents the value of the investment after treating signals
    as entrance and exit points during the dateset indicated.
    '''

    dates = dates or core.current_dates()
    dv = dates.vec
    fd = dates.first_date()
    ld = dates.last_date()
    alternate_investment = alternate_investment or constant(1)

    alt_vals = series_dates_values(alternate_investment, dates)
    inv_vals = series_dates_values(s, dates)
    sig_vals = series_dates_values(signals, dates)

    outv = [None for dt in range(fd, ld + 1)]

    product = 1.0
    buy = True
    prev_inv = prev_alt = None
    first_sig_ob = first_ob(signals, dates=dates)
    if not first_sig_ob:
        raise Exception("signal series is empty")

    for (dt, alt, inv, sig) in zip(dv, alt_vals, inv_vals, sig_vals):
        if dt < first_sig_ob[0]:
            continue
        if sig or (core.is_valid_num(alt) and core.is_valid_num(inv)):
            change = None
            if not core.is_valid_num(inv):
                raise Exception("missing investment observation at {}".format(
                    core.jdate_to_text(dt)))
            if not core.is_valid_num(alt):
                raise Exception(
                    "missing alternate_investment observation at {}".format(
                        core.jdate_to_text(dt)))
            if buy:
                if prev_inv:
                    change = inv / prev_inv
                else:
                    change = 1.0
            else:
                if prev_alt:
                    change = alt / prev_alt
                else:
                    change = 1.0

            # prior to having signal, nothing was invested
            if dt <= first_sig_ob[0]:
                change = 1.0

            new_buy = (sig > 0) if sig else buy
            new_product = product * change

            outv[dt - fd] = new_product * initial_value

            product = new_product
            buy = new_buy
            prev_inv = inv
            prev_alt = alt

    return core.vector_series(outv, fd)