I was wondering if anyone here wants to take a look at it and/or try to convert it to quantconnect? It performed really well in backtesting on quantopian.
from quantopian.pipeline import Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.factors import SimpleMovingAverage, AverageDollarVolume
from quantopian.pipeline.filters.morningstar import IsPrimaryShare
import numpy as np # needed for NaN handling
import math # ceil and floor are useful for rounding
from itertools import cycle
def initialize(context):
# set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.50))
set_slippage(
slippage.VolumeShareSlippage(
volume_limit=.20,
price_impact=0.0))
# set_slippage(slippage.FixedSlippage(spread=0.00))
set_commission(commission.PerTrade(cost=0.00))
# set_slippage(slippage.FixedSlippage(spread=0.00))
set_long_only()
context.MaxCandidates = 100
context.MaxBuyOrdersAtOnce = 30
context.MyLeastPrice = 3.00
context.MyMostPrice = 25.00
context.MyFireSalePrice = context.MyLeastPrice
context.MyFireSaleAge = 6
# over simplistic tracking of position age
context.age = {}
print(len(context.portfolio.positions))
# Rebalance
EveryThisManyMinutes = 10
TradingDayHours = 6.5
TradingDayMinutes = int(TradingDayHours * 60)
for minutez in range(
1,
TradingDayMinutes,
EveryThisManyMinutes
):
schedule_function(
my_rebalance,
date_rules.every_day(),
time_rules.market_open(
minutes=minutez))
# Prevent excessive logging of canceled orders at market close.
schedule_function(
cancel_open_orders,
date_rules.every_day(),
time_rules.market_close(
hours=0,
minutes=1))
# Record variables at the end of each day.
schedule_function(
my_record_vars,
date_rules.every_day(),
time_rules.market_close())
# Create our pipeline and attach it to our algorithm.
my_pipe = make_pipeline(context)
attach_pipeline(my_pipe, 'my_pipeline')
def make_pipeline(context):
"""
Create our pipeline.
"""
# Filter for primary share equities. IsPrimaryShare is a built-in filter.
primary_share = IsPrimaryShare()
# Equities listed as common stock (as opposed to, say, preferred stock).
# 'ST00000001' indicates common stock.
common_stock = morningstar.share_class_reference.security_type.latest.eq(
'ST00000001')
# Non-depositary receipts. Recall that the ~ operator inverts filters,
# turning Trues into Falses and vice versa
not_depositary = ~morningstar.share_class_reference.is_depositary_receipt.latest
# Equities not trading over-the-counter.
not_otc = ~morningstar.share_class_reference.exchange_id.latest.startswith(
'OTC')
# Not when-issued equities.
not_wi = ~morningstar.share_class_reference.symbol.latest.endswith('.WI')
# Equities without LP in their name, .matches does a match using a regular
# expression
not_lp_name = ~morningstar.company_reference.standard_name.latest.matches(
'.* L[. ]?P.?$')
# Equities with a null value in the limited_partnership Morningstar
# fundamental field.
not_lp_balance_sheet = morningstar.balance_sheet.limited_partnership.latest.isnull()
# Equities whose most recent Morningstar market cap is not null have
# fundamental data and therefore are not ETFs.
have_market_cap = morningstar.valuation.market_cap.latest.notnull()
# At least a certain price
price = USEquityPricing.close.latest
AtLeastPrice = (price >= context.MyLeastPrice)
AtMostPrice = (price <= context.MyMostPrice)
# Filter for stocks that pass all of our previous filters.
tradeable_stocks = (
primary_share
& common_stock
& not_depositary
& not_otc
& not_wi
& not_lp_name
& not_lp_balance_sheet
& have_market_cap
& AtLeastPrice
& AtMostPrice
)
LowVar = 6
HighVar = 40
log.info(
'''
Algorithm initialized variables:
context.MaxCandidates %s
LowVar %s
HighVar %s''' %
(context.MaxCandidates, LowVar, HighVar))
# High dollar volume filter.
base_universe = AverageDollarVolume(
window_length=20,
mask=tradeable_stocks
).percentile_between(LowVar, HighVar)
# Short close price average.
ShortAvg = SimpleMovingAverage(
inputs=[USEquityPricing.close],
window_length=3,
mask=base_universe
)
# Long close price average.
LongAvg = SimpleMovingAverage(
inputs=[USEquityPricing.close],
window_length=45,
mask=base_universe
)
percent_difference = (ShortAvg - LongAvg) / LongAvg
# Filter to select securities to long.
stocks_worst = percent_difference.bottom(context.MaxCandidates)
securities_to_trade = (stocks_worst)
return Pipeline(
columns={
'stocks_worst': stocks_worst
},
screen=(securities_to_trade),
)
def my_compute_weights(context):
"""
Compute ordering weights.
"""
# Compute even target weights for our long positions and short positions.
stocks_worst_weight = 1.00 / len(context.stocks_worst)
return stocks_worst_weight
def before_trading_start(context, data):
# Gets our pipeline output every day.
context.output = pipeline_output('my_pipeline')
context.stocks_worst = context.output[
context.output['stocks_worst']].index.tolist()
context.stocks_worst_weight = my_compute_weights(context)
context.MyCandidate = cycle(context.stocks_worst)
context.LowestPrice = context.MyLeastPrice # reset beginning of day
print(len(context.portfolio.positions))
for stock in context.portfolio.positions:
CurrPrice = float(data.current([stock], 'price'))
if CurrPrice < context.LowestPrice:
context.LowestPrice = CurrPrice
if stock in context.age:
context.age[stock] += 1
else:
context.age[stock] = 1
for stock in context.age:
if stock not in context.portfolio.positions:
context.age[stock] = 0
message = 'stock.symbol: {symbol} : age: {age}'
log.info(message.format(symbol=stock.symbol, age=context.age[stock]))
pass
def my_rebalance(context, data):
BuyFactor = .99
SellFactor = 1.01
cash = context.portfolio.cash
cancel_open_buy_orders(context, data)
# Order sell at profit target in hope that somebody actually buys it
for stock in context.portfolio.positions:
if not get_open_orders(stock):
StockShares = context.portfolio.positions[stock].amount
CurrPrice = float(data.current([stock], 'price'))
CostBasis = float(context.portfolio.positions[stock].cost_basis)
SellPrice = float(
make_div_by_05(
CostBasis *
SellFactor,
buy=False))
if np.isnan(SellPrice):
pass # probably best to wait until nan goes away
elif (stock in context.age and context.age[stock] == 1):
pass
elif (
stock in context.age
and context.MyFireSaleAge <= context.age[stock]
and (
context.MyFireSalePrice > CurrPrice
or CostBasis > CurrPrice
)
):
if (stock in context.age and context.age[stock] < 2):
pass
elif stock not in context.age:
context.age[stock] = 1
else:
SellPrice = float(
make_div_by_05(.95 * CurrPrice, buy=False))
order(stock, -StockShares,
style=LimitOrder(SellPrice)
)
else:
if (stock in context.age and context.age[stock] < 2):
pass
elif stock not in context.age:
context.age[stock] = 1
else:
order(stock, -StockShares,
style=LimitOrder(SellPrice)
)
WeightThisBuyOrder = float(1.00 / context.MaxBuyOrdersAtOnce)
for ThisBuyOrder in range(context.MaxBuyOrdersAtOnce):
stock = next(context.MyCandidate)
PH = data.history([stock], 'price', 20, '1d')
PH_Avg = float(PH.mean())
CurrPrice = float(data.current([stock], 'price'))
if np.isnan(CurrPrice):
pass # probably best to wait until nan goes away
else:
if CurrPrice > float(1.25 * PH_Avg):
BuyPrice = float(CurrPrice)
else:
BuyPrice = float(CurrPrice * BuyFactor)
BuyPrice = float(make_div_by_05(BuyPrice, buy=True))
StockShares = int(WeightThisBuyOrder * cash / BuyPrice)
order(stock, StockShares,
style=LimitOrder(BuyPrice)
)
# if cents not divisible by .05, round down if buy, round up if sell
def make_div_by_05(s, buy=False):
s *= 20.00
s = math.floor(s) if buy else math.ceil(s)
s /= 20.00
return s
def my_record_vars(context, data):
"""
Record variables at the end of each day.
"""
# Record our variables.
record(leverage=context.account.leverage)
record(positions=len(context.portfolio.positions))
if 0 < len(context.age):
MaxAge = context.age[max(
list(context.age.keys()), key=(lambda k: context.age[k]))]
print(MaxAge)
record(MaxAge=MaxAge)
record(LowestPrice=context.LowestPrice)
def log_open_order(StockToLog):
oo = get_open_orders()
if len(oo) == 0:
return
for stock, orders in oo.items():
if stock == StockToLog:
for o in orders:
message = 'Found open order for {amount} shares in {stock}'
log.info(message.format(amount=o.amount, stock=stock))
def log_open_orders():
oo = get_open_orders()
if len(oo) == 0:
return
for stock, orders in oo.items():
for o in orders:
message = 'Found open order for {amount} shares in {stock}'
log.info(message.format(amount=o.amount, stock=stock))
def cancel_open_buy_orders(context, data):
oo = get_open_orders()
if len(oo) == 0:
return
for stock, orders in oo.items():
for o in orders:
# message = 'Canceling order of {amount} shares in {stock}'
# log.info(message.format(amount=o.amount, stock=stock))
if 0 < o.amount: # it is a buy order
cancel_order(o)
def cancel_open_orders(context, data):
oo = get_open_orders()
if len(oo) == 0:
return
for stock, orders in oo.items():
for o in orders:
# message = 'Canceling order of {amount} shares in {stock}'
# log.info(message.format(amount=o.amount, stock=stock))
cancel_order(o)
# This is the every minute stuff
def handle_data(context, data):
pass
Valery T
>>It performed really well in backtesting on quantopian.<<
Performance numbers?
The algo uses limit orders, which means that backtesting might not match real life.
Iulian Dragan
I only remember that sharpe and sortino ratios were over 3 and that some users on the forum were confirming good performance in their alpaca accounts. I cant post the quantopian url here since they shut down their forum as well, it appears. Anyways Alpaca is not upen to non US citizens so i was not able to confirm live results.
Derek Melchin
Hi Iulian,
To get familiar with our API and start translating this to run on QC, we recommend reviewing our Quantopian Migration docs and completing the Bootcamp lessons.
Best,
Derek Melchin
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
Kristofferson V Tandoc
Hi Iulian Dragan ,
I can probably make an attempt if you can provide more information about the setup? No guarantee though.
Kris
TBird
Kristofferson V Tandoc keep in mind that these posts are all years old and generally very few people even look at this stuff anymore, except for the two of us! 😂
Anyway, I copied the code into ChatGPT, which gave me a nice high level write up on what this Quantopian strategy did. rather than porting this over, writing it from scratch on QC may be faster.
—
This code is a trading algorithm that uses the Quantopian platform. The algorithm uses a pipeline to screen and filter stocks based on certain criteria, such as their price, volume, and fundamental data. The pipeline is created using the `make_pipeline` function, which defines various filters and factors.
The algorithm selects stocks to long based on the difference between a short-term moving average and a long-term moving average. Specifically, the code calculates the percent difference between the short-term moving average and long-term moving average for each stock, and then selects the `context.MaxCandidates` stocks with the lowest percent difference.
The algorithm rebalances the portfolio every `EveryThisManyMinutes` minutes during trading hours, with a maximum of `context.MaxBuyOrdersAtOnce` buy orders at any one time. It also tracks the age of each position and sells positions that have been held for more than `context.MyFireSaleAge` trading days if the stock price falls below `context.MyFireSalePrice`.
Overall, this algorithm is a simple long-only strategy that uses moving averages to identify stocks to buy and sell. Note that this code is only part of a full algorithm and may not work as intended without additional code.
Kristofferson V Tandoc
Hello Iulian Dragan , Valery T , Derek Melchin , and TBird ,
Here is my attempt into translating the originally Quantopian algorithm into the QC platform. I don't really understand the Quantopian algo setup that well, so this might explain why it's not picking up any symbols in the universe selection.
To my understanding, the Quantopian algorithm follows these steps:
Key features of the algorithm:
Feel free to play around?
Kristofferson V Tandoc
Kristofferson V Tandoc
I don't know why an error is returned when I try to post the backtest. I posted the code above instead.
Kristofferson V Tandoc
Here is another iteration of the same Quantopian model.
It's basically a mean reversion model using 3 and 45-day moving averages.The algorithm selects stocks with the worst performance by comparing their short-term (3-day) and long-term (45-day) moving averages. The stocks are filtered using coarse and fine selection functions, and rebalances its portfolio every 10 minutes.
It sells stocks when they reach a profit target of 5% or when a fire sale condition is met. The fire sale condition is triggered if the stock's age in the portfolio is greater than or equal to 7 days and its price is below 95% of the average purchase price. It then plots the total unrealized profit and the number of invested securities for visualization.
I'm not sure why it's not picking up any symbols in the universe.
Kristofferson V Tandoc
Iulian Dragan
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!