# Template algorithm for the insiders challenge. Based on an algorithm provided by Leo M
# The algo uses documented example from: https://www.quantopian.com/docs/data-reference/ownership_aggregated_insider_transactions
from quantopian.algorithm import attach_pipeline, pipeline_output
import quantopian.optimize as opt
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.domain import US_EQUITIES
# Form 3 transactions
from quantopian.pipeline.data.factset.ownership import Form3AggregatedTrades
# Form 4 and Form 5 transactions
from quantopian.pipeline.data.factset.ownership import Form4and5AggregatedTrades
import pandas as pd
import numpy as np
def initialize(context):
"""
Called once at the start of the algorithm.
"""
# Normally a contest algo uses the default commission and slippage
# This is unique and only required for this 'mini-contest'
set_commission(commission.PerShare(cost=0.000, min_trade_cost=0))
set_slippage(slippage.FixedSlippage(spread=0))
# Rebalance every day, 1 hour after market open.
schedule_function(
rebalance,
date_rules.week_start(),
time_rules.market_close(),
)
# Create our dynamic stock selector.
attach_pipeline(make_pipeline(context), 'pipeline')
# Record any custom data at the end of each day
schedule_function(record_positions,
date_rules.week_start(),
time_rules.market_close(),
)
def create_factor():
# Base universe set to the QTradableStocksUS
qtu = QTradableStocksUS()
insider_txns_form3_90d = Form3AggregatedTrades.slice(False, 90)
insider_txns_form4and5_90d = Form4and5AggregatedTrades.slice(False, 90)
insider_txns_form3_30d = Form3AggregatedTrades.slice(False, 30)
insider_txns_form4and5_30d = Form4and5AggregatedTrades.slice(False, 30)
insider_txns_form3_7d = Form3AggregatedTrades.slice(False, 7)
insider_txns_form4and5_7d = Form4and5AggregatedTrades.slice(False, 7)
insider_txns_form3_1d = Form3AggregatedTrades.slice(False, 1)
insider_txns_form4and5_1d = Form4and5AggregatedTrades.slice(False, 1)
# From each DataSet, extract the number of unique buyers and unique sellers.
# We do not need to include unique sellers using Form 3, because Form 3 is
# an initial ownership filing, and so there are no sellers using Form 3.
unique_filers_form3_90d = insider_txns_form3_90d.num_unique_filers.latest
unique_buyers_form4and5_90d = insider_txns_form4and5_90d.num_unique_buyers.latest
unique_sellers_form4and5_90d = insider_txns_form4and5_90d.num_unique_sellers.latest
unique_filers_form3_30d = insider_txns_form3_30d.num_unique_filers.latest
unique_buyers_form4and5_30d = insider_txns_form4and5_30d.num_unique_buyers.latest
unique_sellers_form4and5_30d = insider_txns_form4and5_30d.num_unique_sellers.latest
unique_filers_form3_7d = insider_txns_form3_7d.num_unique_filers.latest
unique_buyers_form4and5_7d = insider_txns_form4and5_7d.num_unique_buyers.latest
unique_sellers_form4and5_7d = insider_txns_form4and5_7d.num_unique_sellers.latest
unique_filers_form3_1d = insider_txns_form3_1d.num_unique_filers.latest
unique_buyers_form4and5_1d = insider_txns_form4and5_1d.num_unique_buyers.latest
unique_sellers_form4and5_1d = insider_txns_form4and5_1d.num_unique_sellers.latest
# Sum the unique buyers from each form together.
unique_buyers_90d = unique_filers_form3_90d + unique_buyers_form4and5_90d
unique_sellers_90d = unique_sellers_form4and5_90d
unique_buyers_30d = unique_filers_form3_30d + unique_buyers_form4and5_30d
unique_sellers_30d = unique_sellers_form4and5_30d
unique_buyers_7d = unique_filers_form3_7d + unique_buyers_form4and5_7d
unique_sellers_7d = unique_sellers_form4and5_7d
unique_buyers_1d = unique_filers_form3_1d + unique_buyers_form4and5_1d
unique_sellers_1d = unique_sellers_form4and5_1d
# Compute the fractions of insiders buying and selling.
#frac_insiders_buying_90d = unique_buyers_90d / (unique_buyers_90d + unique_sellers_90d)
#frac_insiders_selling_90d = unique_sellers_90d / (unique_buyers_90d + unique_sellers_90d)
# Compute the absolute value of insiders buying and selling over last n days.
unique_buyers_minus_sellers_90d=unique_buyers_90d - unique_sellers_90d
unique_buyers_minus_sellers_30d=unique_buyers_30d - unique_sellers_30d
unique_buyers_minus_sellers_7d=unique_buyers_7d - unique_sellers_7d
unique_buyers_minus_sellers_1d=unique_buyers_1d - unique_sellers_1d
# compute factor as buying-selling rank zscores
alpha_factor = unique_buyers_minus_sellers_7d*2 - unique_buyers_minus_sellers_30d*1.1 - unique_buyers_minus_sellers_90d*0.9
# Add the sentiment factor to a pipeline.
pipe = Pipeline(
columns={
'alpha_factor': alpha_factor,
#'frac_insiders_buying_90d' : frac_insiders_buying_90d,
#'frac_insiders_selling_90d' : frac_insiders_selling_90d,
#'unique_filers_form3_90d' : unique_filers_form3_90d,
#'unique_buyers_form4and5_90d' : unique_buyers_form4and5_90d,
#'unique_sellers_form4and5_90d' : unique_sellers_form4and5_90d
},
domain=US_EQUITIES,
)
screen = qtu & ~alpha_factor.isnull() & alpha_factor.isfinite()
return alpha_factor, screen
def make_pipeline(context):
alpha_factor, screen = create_factor()
# Winsorize to remove extreme outliers
#alpha_winsorized = alpha_factor.winsorize(min_percentile=0.00,
# max_percentile=1.00,
# mask=screen)
# Zscore and rank to get long and short (positive and negative) alphas to use as weights
#alpha_rank = alpha_winsorized.rank().zscore()
return Pipeline(columns={'alpha_factor': alpha_factor},
screen=screen, domain=US_EQUITIES)
def rebalance(context, data):
# Get the alpha factor data from the pipeline output
output = pipeline_output('pipeline')
alpha_factor = output.alpha_factor
log.info(alpha_factor)
# Weight securities by their alpha factor
# Divide by the abs of total weight to create a leverage of 1
weights = alpha_factor / alpha_factor.abs().sum()
# Must use TargetWeights as an objective
order_optimal_portfolio(
objective=opt.TargetWeights(weights),
constraints=[],
)
def record_positions(context, data):
pos = pd.Series()
for position in context.portfolio.positions.values():
pos.loc[position.sid] = position.amount
pos /= pos.abs().sum()
# Show quantiles of the daily holdings distribution
# to show if weights are being squashed to equal weight
# or whether they have a nice range of sensitivity.
quantiles = pos.quantile([.05, .25, .5, .75, .95]) * 100
record(q05=quantiles[.05])
record(q25=quantiles[.25])
record(q50=quantiles[.5])
record(q75=quantiles[.75])
record(q95=quantiles[.95])