├── .DS_Store ├── .gitattributes ├── Algorithm ├── .DS_Store ├── longshort_avgsentiment.py ├── longshort_sentimentsignal.py ├── longshort_sma30rank.py └── longshort_template.py ├── Assets ├── .DS_Store ├── CummulativeReturns.png ├── GitHub-Mark.png ├── MeanReturns.png ├── PeriodReturns.png ├── Pipeline.png ├── ResearchWorkflow.png └── RollingVolatility.png ├── Backtesting └── Backtest_Results.ipynb ├── Exploratory Analysis ├── .DS_Store ├── FactorAnalysis_AverageSentimentSignal.ipynb └── Research.ipynb ├── Poster ├── ResearchPoster.indd └── ResearchPoster.pdf └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-language=Python -------------------------------------------------------------------------------- /Algorithm/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Algorithm/.DS_Store -------------------------------------------------------------------------------- /Algorithm/longshort_avgsentiment.py: -------------------------------------------------------------------------------- 1 | from quantopian.algorithm import attach_pipeline, pipeline_output 2 | from quantopian.pipeline import Pipeline 3 | from quantopian.pipeline.data.builtin import USEquityPricing 4 | from quantopian.pipeline.factors import CustomFactor 5 | from quantopian.pipeline.data.sentdex import sentiment_free as sentdex 6 | 7 | import pandas as pd 8 | import numpy as np 9 | 10 | class AverageSentiment(CustomFactor): 11 | def compute(self, today, assets, out, impact): 12 | np.mean(impact, axis=0, out=out) 13 | 14 | class AverageDailyDollarVolumeTraded(CustomFactor): 15 | window_length = 20 16 | inputs = [USEquityPricing.close, USEquityPricing.volume] 17 | def compute(self, today, assets, out, close_price, volume): 18 | out[:] = np.mean(close_price * volume, axis=0) 19 | 20 | def initialize(context): 21 | window_length = 3 22 | pipe = Pipeline() 23 | pipe = attach_pipeline(pipe, name='Sentiment_Pipe') 24 | dollar_volume = AverageDailyDollarVolumeTraded() 25 | filter = (dollar_volume > 10**7) 26 | 27 | pipe.add(AverageSentiment(inputs=[sentdex.sentiment_signal], 28 | window_length=window_length), 'Average_Sentiment') 29 | pipe.set_screen(filter) 30 | context.longs = None 31 | context.shorts = None 32 | 33 | schedule_function(rebalance, date_rules.every_day(), time_rules.market_open(hours=1)) 34 | set_commission(commission.PerShare(cost=0, min_trade_cost=0)) 35 | set_slippage(slippage.FixedSlippage(spread=0)) 36 | 37 | def before_trading_start(context, data): 38 | results = pipeline_output('Sentiment_Pipe').dropna() 39 | 40 | longs = results[results['Average_Sentiment'] > 0] 41 | shorts = results[results['Average_Sentiment'] < 0] 42 | 43 | long_ranks = longs['Average_Sentiment'].rank().order() 44 | short_ranks = shorts['Average_Sentiment'].rank().order() 45 | 46 | num_stocks = min([25, len(long_ranks.index), len(short_ranks.index)]) 47 | 48 | context.longs = long_ranks.head(num_stocks) 49 | context.shorts = short_ranks.tail(num_stocks) 50 | 51 | update_universe(context.longs.index | context.shorts.index) 52 | 53 | def handle_data(context, data): 54 | #num_positions = [pos for pos in context.portfolio.positions 55 | # if context.portfolio.positions[pos].amount != 0] 56 | record(lever=context.account.leverage, 57 | exposure=context.account.net_leverage, 58 | num_pos=len(context.portfolio.positions), 59 | oo=len(get_open_orders())) 60 | 61 | 62 | def rebalance(context, data): 63 | 64 | for secuhttps://www.quantopian.com/algorithmsrity in context.shorts.index: 65 | if get_open_orders(security): 66 | continue 67 | if security in data: 68 | order_target_percent(security, -1.0 / len(context.shorts)) 69 | 70 | for security in context.longs.index: 71 | if get_open_orders(security): 72 | continue 73 | if security in data: 74 | order_target_percent(security, 1.0 / len(context.longs)) 75 | 76 | for security in context.portfolio.positions: 77 | if get_open_orders(security): 78 | continue 79 | if security in data: 80 | if security not in (context.longs.index | context.shorts.index): 81 | order_target_percent(security, 0) 82 | 83 | -------------------------------------------------------------------------------- /Algorithm/longshort_sentimentsignal.py: -------------------------------------------------------------------------------- 1 | from quantopian.pipeline import Pipeline 2 | from quantopian.algorithm import attach_pipeline, pipeline_output 3 | from quantopian.pipeline.filters.morningstar import Q1500US 4 | from quantopian.pipeline.data.sentdex import sentiment_free 5 | 6 | def initialize(context): 7 | """ 8 | Called once at the start of the algorithm. 9 | """ 10 | # Rebalance every day, 1 hour after market open. 11 | schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_open(hours=1)) 12 | 13 | # Record tracking variables at the end of each day. 14 | schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close()) 15 | 16 | # Create our dynamic stock selector. 17 | attach_pipeline(make_pipeline(), 'my_pipeline') 18 | 19 | set_commission(commission.PerTrade(cost=0.001)) 20 | 21 | 22 | 23 | def make_pipeline(): 24 | 25 | # 5-day sentiment moving average factor. 26 | sentiment_factor = sentiment_free.sentiment_signal.latest 27 | 28 | # Our universe is made up of stocks that have a non-null sentiment signal and are in the Q1500US. 29 | universe = (Q1500US() 30 | & sentiment_factor.notnull()) 31 | 32 | # A classifier to separate the stocks into quantiles based on sentiment rank. 33 | sentiment_quantiles = sentiment_factor.rank(mask=universe, method='average').quantiles(2) 34 | 35 | # Go short the stocks in the 0th quantile, and long the stocks in the 2nd quantile. 36 | pipe = Pipeline( 37 | columns={ 38 | 'sentiment': sentiment_quantiles, 39 | 'longs': (sentiment_factor >=5), 40 | 'shorts': (sentiment_factor<=-2), 41 | }, 42 | screen=universe 43 | ) 44 | 45 | return pipe 46 | 47 | 48 | 49 | def before_trading_start(context, data): 50 | try: 51 | """ 52 | Called every day before market open. 53 | """ 54 | context.output = pipeline_output('my_pipeline') 55 | 56 | # These are the securities that we are interested in trading each day. 57 | context.security_list = context.output.index.tolist() 58 | except Exception as e: 59 | print(str(e)) 60 | 61 | 62 | def my_rebalance(context,data): 63 | """ 64 | Place orders according to our schedule_function() timing. 65 | """ 66 | 67 | # Compute our portfolio weights. 68 | long_secs = context.output[context.output['longs']].index 69 | long_weight = 0.5 / len(long_secs) 70 | 71 | short_secs = context.output[context.output['shorts']].index 72 | short_weight = -0.5 / len(short_secs) 73 | 74 | # Open our long positions. 75 | for security in long_secs: 76 | if data.can_trade(security): 77 | order_target_percent(security, long_weight) 78 | 79 | # Open our short positions. 80 | for security in short_secs: 81 | if data.can_trade(security): 82 | order_target_percent(security, short_weight) 83 | 84 | # Close positions that are no longer in our pipeline. 85 | for security in context.portfolio.positions: 86 | if data.can_trade(security) and security not in long_secs and security not in short_secs: 87 | order_target_percent(security, 0) 88 | 89 | 90 | def my_record_vars(context, data): 91 | """ 92 | Plot variables at the end of each day. 93 | """ 94 | long_count = 0 95 | short_count = 0 96 | 97 | for position in context.portfolio.positions.itervalues(): 98 | if position.amount > 0: 99 | long_count += 1 100 | if position.amount < 0: 101 | short_count += 1 102 | 103 | # Plot the counts 104 | record(num_long=long_count, num_short=short_count, leverage=context.account.leverage) -------------------------------------------------------------------------------- /Algorithm/longshort_sma30rank.py: -------------------------------------------------------------------------------- 1 | # This algorithm implements the long-short equity strategy ranked on the 2 | # 30 day Sentiment Moving Average. 3 | 4 | # Imports 5 | import numpy as np 6 | import pandas as pd 7 | from quantopian.pipeline.filters import Q1500US 8 | import quantopian.optimize as opt 9 | from quantopian.algorithm import attach_pipeline, pipeline_output, order_optimal_portfolio 10 | from quantopian.pipeline import Pipeline 11 | from quantopian.pipeline.factors import SimpleMovingAverage, AverageDollarVolume, RollingLinearRegressionOfReturns 12 | from quantopian.pipeline.data.builtin import USEquityPricing 13 | from quantopian.pipeline.data import morningstar 14 | from quantopian.pipeline.filters.morningstar import IsPrimaryShare 15 | from quantopian.pipeline.classifiers.morningstar import Sector 16 | from quantopian.pipeline.data.psychsignal import stocktwits 17 | from quantopian.pipeline.data.sentdex import sentiment_free 18 | 19 | # Constraint Parameters 20 | MAX_GROSS_EXPOSURE = 1.0 21 | NUM_LONG_POSITIONS = 300 22 | NUM_SHORT_POSITIONS = 300 23 | 24 | # Max Position Size 25 | MAX_SHORT_POSITION_SIZE = 2*1.0/(NUM_LONG_POSITIONS + NUM_SHORT_POSITIONS) 26 | MAX_LONG_POSITION_SIZE = 2*1.0/(NUM_LONG_POSITIONS + NUM_SHORT_POSITIONS) 27 | 28 | # Risk Exposures 29 | MAX_SECTOR_EXPOSURE = 0.10 30 | MAX_BETA_EXPOSURE = 0.20 31 | 32 | def make_pipeline(): 33 | 34 | # Sector 35 | sector = Sector() 36 | 37 | # Equity Filters 38 | mkt_cap_filter = morningstar.valuation.market_cap.latest >= 500000000 39 | price_filter = USEquityPricing.close.latest >= 5 40 | nan_filter = sentiment_free.sentiment_signal.latest.notnull() 41 | 42 | # Universe 43 | universe = Q1500US() & price_filter & mkt_cap_filter & nan_filter 44 | 45 | # Rank 46 | sentiment_signal = sentiment_free.sentiment_signal.latest 47 | sma_30 = SimpleMovingAverage(inputs= 48 | [sentiment_free.sentiment_signal], 49 | window_length=30, mask=universe) 50 | 51 | combined_rank = ( 52 | sentiment_signal + 53 | sma_30.rank(mask=universe).zscore()) 54 | 55 | # Long and Short Positions 56 | longs = combined_rank.top(NUM_LONG_POSITIONS) 57 | shorts = combined_rank.bottom(NUM_SHORT_POSITIONS) 58 | 59 | long_short_screen = (longs | shorts) 60 | 61 | # Bloomberg Beta Implementation 62 | beta = 0.66*RollingLinearRegressionOfReturns( 63 | target=sid(8554), 64 | returns_length=5, 65 | regression_length=260, 66 | mask=long_short_screen).beta + 0.33*1.0 67 | 68 | pipe = Pipeline() 69 | pipe.add(longs, 'longs') 70 | pipe.add(shorts, 'shorts') 71 | pipe.add(combined_rank, 'combined_rank') 72 | pipe.add(sentiment_free.sentiment_signal.latest, 'sentiment_signal') 73 | pipe.add(sma_30, 'sma_30') 74 | pipe.add(sector, 'sector') 75 | pipe.add(beta, 'market_beta') 76 | pipe.set_screen(universe) 77 | 78 | return pipe 79 | 80 | 81 | def initialize(context): 82 | # Slippage and Commissions 83 | set_commission(commission.PerShare(cost=0.0, min_trade_cost=0)) 84 | set_slippage(slippage.VolumeShareSlippage(volume_limit=1, price_impact=0)) 85 | context.spy = sid(8554) 86 | 87 | attach_pipeline(make_pipeline(), 'longshort_sma30rank') 88 | 89 | # Rebalance Function 90 | schedule_function(func=rebalance, 91 | date_rule=date_rules.month_start(), 92 | time_rule=time_rules.market_open(hours=0,minutes=30), 93 | half_days=True) 94 | 95 | # End of the day portfolio variables 96 | schedule_function(func=recording_statements, 97 | date_rule=date_rules.every_day(), 98 | time_rule=time_rules.market_close(), 99 | half_days=True) 100 | 101 | 102 | def before_trading_start(context, data): 103 | # Call pipeline_output to get the output 104 | # Note: this is a dataframe where the index is the SIDs for all 105 | # securities to pass my screen and the columns are the factors 106 | # added to the pipeline object above 107 | context.pipeline_data = pipeline_output('longshort_sma30rank') 108 | 109 | 110 | def recording_statements(context, data): 111 | # Plot the number of positions over time. 112 | record(num_positions=len(context.portfolio.positions)) 113 | 114 | 115 | # Called at the start of every month in order to rebalance 116 | # the longs and shorts lists 117 | def rebalance(context, data): 118 | ### Optimize API 119 | pipeline_data = context.pipeline_data 120 | 121 | ### Extract from pipeline any specific risk factors you want 122 | # to neutralize that you have already calculated 123 | risk_factor_exposures = pd.DataFrame({ 124 | 'market_beta':pipeline_data.market_beta.fillna(1.0) 125 | }) 126 | # We fill in any missing factor values with a market beta of 1.0. 127 | # We do this rather than simply dropping the values because we have 128 | # want to err on the side of caution. We don't want to exclude 129 | # a security just because it's missing a calculated market beta, 130 | # so we assume any missing values have full exposure to the market. 131 | 132 | 133 | ### Here we define our objective for the Optimize API. We have 134 | # selected MaximizeAlpha because we believe our combined factor 135 | # ranking to be proportional to expected returns. This routine 136 | # will optimize the expected return of our algorithm, going 137 | # long on the highest expected return and short on the lowest. 138 | objective = opt.MaximizeAlpha(pipeline_data.combined_rank) 139 | 140 | ### Define the list of constraints 141 | constraints = [] 142 | # Constrain our maximum gross leverage 143 | constraints.append(opt.MaxGrossExposure(MAX_GROSS_EXPOSURE)) 144 | # Require our algorithm to remain dollar neutral 145 | constraints.append(opt.DollarNeutral()) 146 | # Add a sector neutrality constraint using the sector 147 | # classifier that we included in pipeline 148 | constraints.append( 149 | opt.NetGroupExposure.with_equal_bounds( 150 | labels=pipeline_data.sector, 151 | min=-MAX_SECTOR_EXPOSURE, 152 | max=MAX_SECTOR_EXPOSURE, 153 | )) 154 | # Take the risk factors that you extracted above and 155 | # list your desired max/min exposures to them - 156 | # Here we selection +/- 0.01 to remain near 0. 157 | neutralize_risk_factors = opt.FactorExposure( 158 | loadings=risk_factor_exposures, 159 | min_exposures={'market_beta':-MAX_BETA_EXPOSURE}, 160 | max_exposures={'market_beta':MAX_BETA_EXPOSURE} 161 | ) 162 | constraints.append(neutralize_risk_factors) 163 | 164 | # With this constraint we enforce that no position can make up 165 | # greater than MAX_SHORT_POSITION_SIZE on the short side and 166 | # no greater than MAX_LONG_POSITION_SIZE on the long side. This 167 | # ensures that we do not overly concentrate our portfolio in 168 | # one security or a small subset of securities. 169 | constraints.append( 170 | opt.PositionConcentration.with_equal_bounds( 171 | min=-MAX_SHORT_POSITION_SIZE, 172 | max=MAX_LONG_POSITION_SIZE 173 | )) 174 | 175 | # Put together all the pieces we defined above by passing 176 | # them into the order_optimal_portfolio function. This handles 177 | # all of our ordering logic, assigning appropriate weights 178 | # to the securities in our universe to maximize our alpha with 179 | # respect to the given constraints. 180 | order_optimal_portfolio( 181 | objective=objective, 182 | constraints=constraints, 183 | ) -------------------------------------------------------------------------------- /Algorithm/longshort_template.py: -------------------------------------------------------------------------------- 1 | """This algorithm demonstrates the concept of long-short equity. 2 | It uses two fundamental factors to rank equities in our universe. 3 | It then longs the top of the ranking and shorts the bottom. 4 | For information on long-short equity strategies, please see the corresponding 5 | lecture on our lectures page: 6 | 7 | https://www.quantopian.com/lectures 8 | 9 | WARNING: These factors were selected because they worked in the past over the specific time period we choose. 10 | We do not anticipate them working in the future. In practice finding your own factors is the hardest 11 | part of developing any long-short equity strategy. This algorithm is meant to serve as a framework for testing your own ranking factors. 12 | 13 | This algorithm was developed as part of 14 | Quantopian's Lecture Series. Please direct any 15 | questions, feedback, or corrections to max@quantopian.com 16 | """ 17 | 18 | from quantopian.algorithm import attach_pipeline, pipeline_output, order_optimal_portfolio 19 | from quantopian.pipeline import Pipeline 20 | from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage, AverageDollarVolume, RollingLinearRegressionOfReturns 21 | from quantopian.pipeline.data.builtin import USEquityPricing 22 | from quantopian.pipeline.data import morningstar 23 | from quantopian.pipeline.filters.morningstar import IsPrimaryShare 24 | from quantopian.pipeline.classifiers.morningstar import Sector 25 | 26 | import numpy as np 27 | import pandas as pd 28 | 29 | from quantopian.pipeline.filters import Q1500US 30 | import quantopian.optimize as opt 31 | 32 | # Constraint Parameters 33 | MAX_GROSS_EXPOSURE = 1.0 34 | NUM_LONG_POSITIONS = 300 35 | NUM_SHORT_POSITIONS = 300 36 | 37 | # Here we define the maximum position size that can be held for any 38 | # given stock. If you have a different idea of what these maximum 39 | # sizes should be, feel free to change them. Keep in mind that the 40 | # optimizer needs some leeway in order to operate. Namely, if your 41 | # maximum is too small, the optimizer may be overly-constrained. 42 | MAX_SHORT_POSITION_SIZE = 2*1.0/(NUM_LONG_POSITIONS + NUM_SHORT_POSITIONS) 43 | MAX_LONG_POSITION_SIZE = 2*1.0/(NUM_LONG_POSITIONS + NUM_SHORT_POSITIONS) 44 | 45 | # Risk Exposures 46 | MAX_SECTOR_EXPOSURE = 0.10 47 | MAX_BETA_EXPOSURE = 0.20 48 | 49 | class Momentum(CustomFactor): 50 | """ 51 | Here we define a basic momentum factor using a CustomFactor. We take 52 | the momentum from the past year up until the beginning of this month 53 | and penalize it by the momentum over this month. We are tempering a 54 | long-term trend with a short-term reversal in hopes that we get a 55 | better measure of momentum. 56 | """ 57 | inputs = [USEquityPricing.close] 58 | window_length = 252 59 | 60 | def compute(self, today, assets, out, prices): 61 | out[:] = ((prices[-21] - prices[-252])/prices[-252] - 62 | (.notnull()prices[-1] - prices[-21])/prices[-21]) 63 | 64 | def make_pipeline(): 65 | """ 66 | Create and return our pipeline. 67 | 68 | We break this piece of logic out into its own function to make it easier to 69 | test and modify in isolation. 70 | 71 | In particular, this function can be copy/pasted into research and run by itself. 72 | """ 73 | 74 | # Create our momentum, value, and quality factors 75 | momentum = Momentum() 76 | # By appending .latest to the imported morningstar data, we get builtin Factors 77 | # so there's no need to define a CustomFactor 78 | value = morningstar.income_statement.ebit.latest / morningstar.valuation.enterprise_value.latest 79 | quality = morningstar.operation_ratios.roe.latest 80 | 81 | # Classify all securities by sector so that we can enforce sector neutrality later 82 | sector = Sector() 83 | 84 | # Screen out non-desirable securities by defining our universe. 85 | # Removes ADRs, OTCs, non-primary shares, LP, etc. 86 | # Also sets a minimum $500MM market cap filter and $5 price filter 87 | mkt_cap_filter = morningstar.valuation.market_cap.latest >= 500000000 88 | price_filter = USEquityPricing.close.latest >= 5 89 | 90 | universe = Q1500US() & price_filter & mkt_cap_filter 91 | 92 | # Construct a Factor representing the rank of each asset by our momentum, 93 | # value, and quality metrics. We aggregate them together here using simple 94 | # addition. 95 | # 96 | # By applying a mask to the rank computations, we remove any stocks that failed 97 | # to meet our initial criteria **before** computing ranks. This means that the 98 | # stock with rank 10.0 is the 10th-lowest stock that was included in the Q1500US. 99 | combined_rank = ( 100 | momentum.rank(mask=universe).zscore() + 101 | value.rank(mask=universe).zscore() + 102 | quality.rank(mask=universe).zscore() 103 | ) 104 | 105 | # Build Filters representing the top and bottom 150 stocks by our combined ranking system. 106 | # We'll use these as our tradeable universe each day. 107 | longs = combined_rank.top(NUM_LONG_POSITIONS) 108 | shorts = combined_rank.bottom(NUM_SHORT_POSITIONS) 109 | 110 | # The final output of our pipeline should only include 111 | # the top/bottom 300 stocks by our criteria 112 | long_short_screen = (longs | shorts) 113 | 114 | # Define any risk factors that we will want to neutralize 115 | # We are chiefly interested in market beta as a risk factor so we define it using 116 | # Bloomberg's beta calculation 117 | # Ref: https://www.lib.uwo.ca/business/betasbydatabasebloombergdefinitionofbeta.html 118 | beta = 0.66*RollingLinearRegressionOfReturns( 119 | target=sid(8554), 120 | returns_length=5, 121 | regression_length=260, 122 | mask=long_short_screen 123 | ).beta + 0.33*1.0 124 | 125 | 126 | # Create pipeline 127 | pipe = Pipeline(columns = { 128 | 'longs':longs, 129 | 'shorts':shorts, 130 | 'combined_rank':combined_rank, 131 | 'quality':quality, 132 | 'value':value, 133 | 'momentum':momentum, 134 | 'sector':sector, 135 | 'market_beta':beta 136 | }, 137 | screen = long_short_screen) 138 | return pipe 139 | 140 | 141 | def initialize(context): 142 | # Here we set our slippage and commisions. Set slippage 143 | # and commission to zero to evaulate the signal-generating 144 | # ability of the algorithm independent of these additional 145 | # costs. 146 | set_commission(commission.PerShare(cost=0.0, min_trade_cost=0)) 147 | set_slippage(slippage.VolumeShareSlippage(volume_limit=1, price_impact=0)) 148 | context.spy = sid(8554) 149 | 150 | attach_pipeline(make_pipeline(), 'long_short_equity_template') 151 | 152 | # Schedule my rebalance function 153 | schedule_function(func=rebalance, 154 | date_rule=date_rules.month_start(), 155 | time_rule=time_rules.market_open(hours=0,minutes=30), 156 | half_days=True) 157 | # record my portfolio variables at the end of day 158 | schedule_function(func=recording_statements, 159 | date_rule=date_rules.every_day(), 160 | time_rule=time_rules.market_close(), 161 | half_days=True) 162 | 163 | 164 | def before_trading_start(context, data): 165 | # Call pipeline_output to get the output 166 | # Note: this is a dataframe where the index is the SIDs for all 167 | # securities to pass my screen and the columns are the factors 168 | # added to the pipeline object above 169 | context.pipeline_data = pipeline_output('long_short_equity_template') 170 | 171 | 172 | def recording_statements(context, data): 173 | # Plot the number of positions over time. 174 | record(num_positions=len(context.portfolio.positions)) 175 | 176 | 177 | # Called at the start of every month in order to rebalance 178 | # the longs and shorts lists 179 | def rebalance(context, data): 180 | ### Optimize API 181 | pipeline_data = context.pipeline_data 182 | 183 | ### Extract from pipeline any specific risk factors you want 184 | # to neutralize that you have already calculated 185 | risk_factor_exposures = pd.DataFrame({ 186 | 'market_beta':pipeline_data.market_beta.fillna(1.0) 187 | }) 188 | # We fill in any missing factor values with a market beta of 1.0. 189 | # We do this rather than simply dropping the values because we have 190 | # want to err on the side of caution. We don't want to exclude 191 | # a security just because it's missing a calculated market beta, 192 | # so we assume any missing values have full exposure to the market. 193 | 194 | 195 | ### Here we define our objective for the Optimize API. We have 196 | # selected MaximizeAlpha because we believe our combined factor 197 | # ranking to be proportional to expected returns. This routine 198 | # will optimize the expected return of our algorithm, going 199 | # long on the highest expected return and short on the lowest. 200 | objective = opt.MaximizeAlpha(pipeline_data.combined_rank) 201 | 202 | ### Define the list of constraints 203 | constraints = [] 204 | # Constrain our maximum gross leverage 205 | constraints.append(opt.MaxGrossExposure(MAX_GROSS_EXPOSURE)) 206 | # Require our algorithm to remain dollar neutral 207 | constraints.append(opt.DollarNeutral()) 208 | # Add a sector neutrality constraint using the sector 209 | # classifier that we included in pipeline 210 | constraints.append( 211 | opt.NetGroupExposure.with_equal_bounds( 212 | labels=pipeline_data.sector, 213 | min=-MAX_SECTOR_EXPOSURE, 214 | max=MAX_SECTOR_EXPOSURE, 215 | )) 216 | # Take the risk factors that you extracted above and 217 | # list your desired max/min exposures to them - 218 | # Here we selection +/- 0.01 to remain near 0. 219 | neutralize_risk_factors = opt.FactorExposure( 220 | loadings=risk_factor_exposures, 221 | min_exposures={'market_beta':-MAX_BETA_EXPOSURE}, 222 | max_exposures={'market_beta':MAX_BETA_EXPOSURE} 223 | ) 224 | constraints.append(neutralize_risk_factors) 225 | 226 | # With this constraint we enforce that no position can make up 227 | # greater than MAX_SHORT_POSITION_SIZE on the short side and 228 | # no greater than MAX_LONG_POSITION_SIZE on the long side. This 229 | # ensures that we do not overly concentrate our portfolio in 230 | # one security or a small subset of securities. 231 | constraints.append( 232 | opt.PositionConcentration.with_equal_bounds( 233 | min=-MAX_SHORT_POSITION_SIZE, 234 | max=MAX_LONG_POSITION_SIZE 235 | )) 236 | 237 | # Put together all the pieces we defined above by passing 238 | # them into the order_optimal_portfolio function. This handles 239 | # all of our ordering logic, assigning appropriate weights 240 | # to the securities in our universe to maximize our alpha with 241 | # respect to the given constraints. 242 | order_optimal_portfolio( 243 | objective=objective, 244 | constraints=constraints, 245 | ) -------------------------------------------------------------------------------- /Assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Assets/.DS_Store -------------------------------------------------------------------------------- /Assets/CummulativeReturns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Assets/CummulativeReturns.png -------------------------------------------------------------------------------- /Assets/GitHub-Mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Assets/GitHub-Mark.png -------------------------------------------------------------------------------- /Assets/MeanReturns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Assets/MeanReturns.png -------------------------------------------------------------------------------- /Assets/PeriodReturns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Assets/PeriodReturns.png -------------------------------------------------------------------------------- /Assets/Pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Assets/Pipeline.png -------------------------------------------------------------------------------- /Assets/ResearchWorkflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Assets/ResearchWorkflow.png -------------------------------------------------------------------------------- /Assets/RollingVolatility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Assets/RollingVolatility.png -------------------------------------------------------------------------------- /Exploratory Analysis/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Exploratory Analysis/.DS_Store -------------------------------------------------------------------------------- /Poster/ResearchPoster.indd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Poster/ResearchPoster.indd -------------------------------------------------------------------------------- /Poster/ResearchPoster.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsisaacs/QuantStrategies/66c3b9b35f0f160e8ef039512ae1585a3c330470/Poster/ResearchPoster.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Equity Trading with Sentiment 2 | ### A long-short equity quantitative trading strategy based on sentiment data. 3 | 4 | We sought to develop a profitable long-short equity strategy that uses sentiment analysis data as the ranking factor. To do so, many factors were analyzed using Quantopian’s Alphalens tool which generates a tear-sheet of relevant statistics. An ideal factor has perfect predictive power of relative price movements. The averaged sentiment signal with a window length of 3 days was considered the most viable out 8 other candidates tested. Backtesting the strategy from early 2014 to late 2017 yielded a cumulative return of 42.5% and a Sharpe ratio of 1.33. 5 | 6 | ## Built With 7 | * Python 8 | * Jupyter Notebooks 9 | * [Quantopian](https://www.quantopian.com/) 10 | 11 | ## Authors 12 | * [Josh Isaacson](https://github.com/jsisaacs) 13 | * [Hannah Isaacson](https://github.com/hannahisaacson) --------------------------------------------------------------------------------