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 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 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 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 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)
def obs_to_series(obs, name=None): '''Convert a list of (date,value) tuples to a series.''' valids = [(core.to_jdate(ob[0]), ob[1]) for ob in obs if ob] valids.sort() if not len(valids): return core.series(lambda dt: None, name) fd = valids[0][0] ld = valids[-1][0] vec = [None for x in range(fd, ld + 1)] for (dt, val) in valids: vec[dt - fd] = val return core.vector_series(vec, fd, name=name)
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)))
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 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 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 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 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 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 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)