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)
def f(dt): a_val = af(dt) if core.is_valid_num(a_val): b_val = bf(dt) if core.is_valid_num(b_val): return b_val return False
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(...)")
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)
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))
def allocation_equity_line(allocations, *, initial_value=100): '''Generate an equity line based upon weighted portfolios on multiple dates, either changing the weight or rebalancing. The first value of the equity line begins at the first allocation date.''' portfolios = allocations_to_portfolios(allocations) holdings_by_date = {p.date: p.holdings for p in portfolios} fd = portfolios[0].date holdings = [Holding(CASH, 1)] obs = [] for dt in range(fd, core.today(1)): valuation = value_of_holdings(dt, holdings) new_holdings = holdings_by_date.get(dt) if new_holdings: if not valuation: raise Exception( "missing observation at allocate date {}".format( jdate_to_text(dt))) holdings = new_holdings if core.is_valid_num(valuation) and valuation > 0: obs.append((dt, initial_value * valuation)) return obs_to_series(obs)
def allocations_to_portfolios(allocations): allocations = sorted([normalize_allocation(a) for a in allocations], key=lambda a: a.date) holdings = [Holding(CASH, 1)] result = [] for a in allocations: date = a.date valuation = value_of_holdings(date, holdings) holdings = [] for p in a.portions: f = p.series.f price = f(date) if not core.is_valid_num(price): raise Exception("missing price at {}".format( jdate_to_text(date))) dollars_for_buy = p.amount * valuation shares_to_buy = dollars_for_buy / price holdings.append(Holding(p.series, shares_to_buy)) result.append(Portfolio(date, holdings)) return result
def value_of_holdings(date, holdings): total = 0 for h in holdings: price = h.series.f(date) if not core.is_valid_num(price): return None total = total + (h.shares * price) return total
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
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)))
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)
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
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))
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))
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
def series_map(proc, *seriesz, missing_data_permitted=False, dts=None): '''Analogous to Python's standard map operation, map the arity-compatible function :code:`proc` over one or more series returning a single value on each date.''' def missing_nums(nums): return len(nums) != sum((1 for n in nums if core.is_valid_num(n))) converted = [convert(s) for s in seriesz] dts = dts or dates.current_dates() sfs = [s.f for s in converted] obs = [] for dt in dts: nums = [f(dt) for f in sfs] if (not missing_data_permitted) and missing_nums(nums): continue val = proc(*nums) if core.is_valid_num(val): obs.append((dt, val)) return obs_to_series(obs)
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))
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)
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)
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
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)))
def calibrate(s, init=100, date=None): '''To calibrate a series is to make it value-compatible with another. For example, a spider graph picks a point at which multiple time series are the same value. The default date is the beginning of the dateset and the default value of the series there is 100, making any value in the series equivalent to the percentage of the beginning.''' date = core.to_date(date) or core.first_date() f = s.f val = f(date) if not core.is_valid_num(val): raise Exception("no observation at {}".format(date)) ratio = init / val def fetcher(dt): val = f(dt) if core.is_valid_num(val): val = val * ratio return val return core.series(fetcher, "calibrate({})".format(abbreviate(s)))
def f(dt): a_val = af(dt) b_val = bf(dt) if core.is_valid_num(a_val) and core.is_valid_num(b_val): return op(a_val, b_val)
def f(dt): for i in range(days + 1): val = sf(dt - i) if core.is_valid_num(val): return val return None
def ratio(early, late): return (late / early - 1.0) if (core.is_valid_num(late) and core.is_valid_num(early)) else None
def convert(s): if core.is_valid_num(s): return constant(s) else: return s
def missing_nums(nums): return len(nums) != sum((1 for n in nums if core.is_valid_num(n)))
def valid(dt): return core.is_valid_num(s.f(dt))
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)
def f(dt): val = base_f(dt) return val if (core.is_valid_num(val) and predicate(val)) else None