Exit after X days?

It it possible to setup a time based exit, ie. Exit all positions after 5 days? Or stated another way, exit 5 days after opening a position...

14 responses

Hello Beau,

The short answer is yes, but do you mean 5 calendar days, or 5 trading days? Also, are you planning to write an algorithm to run on daily data, or minutely?

Grant

Hi Grant, I mean 5 trading days, using daily data

Beau,

Here's an example:

from pytz import timezone

def initialize(context):
context.order_submitted = False

def handle_data(context, data):

if not context.order_submitted:
order(sid(24), 50)
context.order_submitted = True
print 'buy order submitted ' + str(get_datetime().astimezone(timezone('US/Eastern')))
order(sid(24), -50)
print 'sell order submitted ' + str(get_datetime().astimezone(timezone('US/Eastern')))


The log output is:

2014-02-10 PRINT buy order submitted 2014-02-09 19:00:00-05:00
2014-02-18 PRINT sell order submitted 2014-02-17 19:00:00-05:00


The transactions list:

2014-02-10 19:00:00     AAPL    BUY     50  $535.99$26,799.50
2014-02-18 19:00:00     AAPL    SELL    -50     $537.32 ($26,866.00)


I don't understand why get_datetime() is returning dates that are shifted by a day...hmm?

In any case, perhaps this provides some guidance.

Grant

14
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
from pytz import timezone

def initialize(context):

context.order_submitted = False

def handle_data(context, data):

if not context.order_submitted:
order(sid(24), 50)
context.order_submitted = True
print 'buy order submitted ' + str(get_datetime().astimezone(timezone('US/Eastern')))

order(sid(24), -50)
print 'sell order submitted ' + str(get_datetime().astimezone(timezone('US/Eastern')))

This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Here's the same backtest, except run on minute data. Timestamps are correct. It seems that something has changed in the daily backtester that shifts that timestamps...I've submitted a question via the Feedback button. In any case, the code I posted should still work for backtesting on daily data. --Grant

14
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
from pytz import timezone

def initialize(context):

context.order_submitted = False

def handle_data(context, data):

if not context.order_submitted:
order(sid(24), 50)
context.order_submitted = True
print 'buy order submitted ' + str(get_datetime().astimezone(timezone('US/Eastern')))

order(sid(24), -50)
print 'sell order submitted ' + str(get_datetime().astimezone(timezone('US/Eastern')))

This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Hello Grant,

I'm sure I've read something about your 'shifted' day in the zipline group but I can't find it. This is in the right area (see: https://github.com/quantopian/zipline/pull/208 )

+def canonicalize_datetime(dt):
+    # Strip out any HHMMSS or timezone info in the user's datetime, so that
+    # all the datetimes we return will be 00:00:00 UTC.
+    return datetime(dt.year, dt.month, dt.day, tzinfo=pytz.utc)


I'm thinking your datetime is stripped to 00:00:00 UTC.and your UTC -5 hours offset is then applied to time your transaction at 19:00 the day before.

P.

Yeah, this behavior may have been in place all along and I just didn't pick up on it. It certainly is confusing, though. If I am interpreting things correctly, all of the datetime stamps of the daily bars are set to 00:00:00 UTC, even though the bar is aggregated at EOD. --Grant

Hi Grant, Peter,

I implemented your code for multiple sids. It does NOT seem to work correctly.
I wanted to buy stocks and hold for given days (e.g. 25 trading days here) and sell them.
It does not have to buy all together and sell them all together. But in this example, I just wanted to see if it holds for given days per each stock.
For example, on 1/2/2013, it is supposed to buy all three stocks in my codes. But as you can see in the transaction details, it buys one and then buy others on a different day. Selling is also not based on the 25 trading days.

Can you correct my codes?

Thank you.

34
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
from pytz import timezone
import datetime
import pytz
import pandas as pd
import numpy as np

def initialize(context):

context.stocks = [ sid(37049), sid(24), sid(16841) ]

context.long_entry = 0
context.long_exit = 0

def handle_data(context, data):

exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern')

if exchange_time.hour != 15 or exchange_time.minute !=58:
return

else:
for stock in context.stocks:

if context.long_entry==0 and context.long_exit==0:
order_target_value(stock, 100000)
context.long_entry = 1
context.long_exit = 0

print 'Long, ' + str(stock) + ', '+ str(get_datetime().astimezone(timezone('US/Eastern')))

elif context.long_entry==0 and context.long_exit==1:
order_target_value(stock, 100000)
context.long_entry = 1
context.long_exit = 0

print 'Long#2, ' + str(stock) + ', ' + str(get_datetime().astimezone(timezone('US/Eastern')))

elif context.long_entry==1 and context.long_exit==0 and context.trading_day_counter == 25:
order_target_percent(stock, 0)
context.long_entry = 0
context.long_exit = 1
print 'Sell, ' + str(stock) + ', ' + str(get_datetime().astimezone(timezone('US/Eastern')))


This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Thanks Kyu, this is where I was eventually heading. I wanted to see how a simple system might work using the following rules:

• Select a universe of stocks where price > 1 and daily volume > 100,000
• If RSI cross below 20 open long position, or If RSI cross above 80 open short position (only if a position doesn't already exist)
• Exit a position after 5 trading days have passed, (just before close of 5th trading day or on open of 6th day?)

This is really just an experiment to test out something Finviz published (http://elite.finviz.com/help/technical-analysis/backtests.ashx). I think this page is only available to paying members but the jist is they tested a bunch of indicators using simple rules using the indicator to signal an entry and holding for X days between 5 and 20. The system above does well according to their research and I wanted to see how it might play out here.

Thanks,
Beau

Kyu,

if you want Grant's code to work, you need to re-initialize the variable context.order_submitted to False when you sell

Hi Kyu,

I took a look at your code and there were a couple issues that prevented it from running correctly. Here is what I found:

if exchange_time.hour != 15 or exchange_time.minute !=58:


This code will run anytime that the exchange hour is not 3PM or the minute is not 58. I think what you intended is to run this once a day (at 3:58PM EST) to check if it's time to exit the position. In that case you would want:
 if exchange_time.hour == 15 and exchange_time.minute ==58: 

Also, your "long" position was only entered once - the first time the algorithm ran. Then it oscillates between "sell" and "long2". I'm not sure if this is your intended behavior, but I wanted to bring it to your attention.

If you (and @Beau) are looking for a basic algo that enters a position once and holds for X days, attached is a simple example. It buys the basket of stocks and holds for 25 days. Keep in mind the stocks are not all bought at once because of the default commission and slippage models. You can turn these off (by setting to zero) if you want to force a different behavior.

I used order_target_percent instead of order_target_value because it is more robust to changes in portfolio size. In this example I allocated 33% to FAS, 33% to Apple, and 33% to Amazon.

Hope that helps!
Alisa

17
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
import pandas as pd
from pytz import timezone
import datetime

def initialize(context):
context.stocks = [sid(37049), sid(24), sid(16841)]  ## FAS, Apple, Amazon
context.last_rebalance = None
context.next_rebalance = None
context.holding_period = 25 #define holding period in days

def handle_data(context, data):

# convert time to EST time
exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern')

#check to see if we have an existing position. And then order our basket of stocks
if context.last_rebalance == None:
for stock in context.stocks:
order_target_percent(stock,0.33)
print(stock)
log.info('Ordering %s' % (stock.symbol))
context.last_rebalance = exchange_time
context.next_rebalance = context.last_rebalance + \
datetime.timedelta(days=context.holding_period)

#time to sell after 25 days and exit position
elif context.last_rebalance is not None and exchange_time >= context.next_rebalance:
for stock in context.stocks:
order_target_percent(stock,0.0)
log.info('Exiting position in %s' % (stock.symbol))

else:
return


This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.
Disclaimer

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 Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian 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.

Hi Grant,

I saw your feedback email and thought I'd share my response here for everyone to see. Your question was about the timestamp in the logs. Particularly, why there's an apparent time discrepancy in the logs:

2013-01-04  PRINT buy order submitted 2013-01-03 19:00:00-05:00


@Peter was correct in his explanation. When you run a backtest in daily mode, the timestamp for the bar is midnight UTC or 00:00:00 of that day. However, when you convert this to the US Eastern timezone, it becomes 19:00:00 EST (7PM) the previous day.

Although this is a cosmetic issue, if we could do this over again, I agree it'd be better to use the timestamp at the close of the market.

Hi Alisa,

Thank you. But transaction details show odd results as below.

2013-01-02 19:00:00 AAPL BUY 601 $542.41$325,988.41
2013-01-02 19:00:00 AMZN BUY 1283 $258.54$331,706.82
2013-01-02 19:00:00 FAS BUY 7640 $43.19$329,994.52
2013-01-28 19:00:00 AAPL SELL -601 $458.40 ($275,498.40)
2013-01-28 19:00:00 AMZN SELL -1283 $268.13 ($344,010.79)
2013-01-28 19:00:00 FAS SELL -7639 $47.98 ($366,488.66)
2013-01-29 19:00:00 FAS SELL -1 $47.43 ($47.43)

Over the past 14 months, there were only 7 transactions. It does not look working correctly.

My (and Beau's) intention was to buy stocks and hold them for given days and then sell.
In my code, it should buy all stocks on 1/2/2013 and sell all of them 25 trading days later. And buy them right next day (or same day) after selling. In other words, roughly saying, this sample algo should buy stocks once every month and sell once every month (or any given days).

The reason I want to code with minute data is that we can add a lot more conditions on this sample algo as Beau wanted. With daily data, all the orders will be filled at the next day close, which I think very unrealistic and odd. And thus, it should be based on minute data. The time to buy or sell is various. I just wanted to buy and sell based on daily close price (near daily close at 15:59), which is very common in real world.

I will work on this and get back later.

Thank you.

Just add the last line to Alisa's code:

order_target_percent(stock,0.0)
log.info('Exiting position in %s' % (stock.symbol))
context.last_rebalance = None

Hi all,

I corrected my code as below.
This sample algo buys stocks once at the close of day (15:59 E.T.) and hold for given days (25 trading days here) and sell them all on the day. And the next day this algo restarts buying. There's no other logics here as this is just a sample code. BTW, I need to simplify some of this code especially by using loop, which I will do later.

This is a little different from those of rebalancing models. Because if we want to struct our algo with a lot of various factors and apply them to each stocks differently, we need to rebalance each stock on different time frame rather than on the same day. This is a kind of those structures. There will be a lot of corrections and adjustment needed later but at this point, this is a good start.

@Beau, I will get back to you later with RSI model. You know Quantopian has set universe.

    set_universe(universe.DollarVolumeUniverse(floor_percentile=98.0,ceiling_percentile=100.0))


The maximum percentile with minute data we can have is 2%, which I think is about 160 stocks only. You can use this or your own universe upto 100 stocks.

Thank you.

34
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
from pytz import timezone
import datetime
import pytz
import pandas as pd
import numpy as np

def initialize(context):

context.stocks = [sid(37049), sid(24), sid(216), sid(35920)]

context.long_entry = 0
context.long_exit = 0

def handle_data(context, data):

exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern')

if exchange_time.hour != 15 or exchange_time.minute !=58:
return

else:

for stock in context.stocks:

if context.long_entry ==0 and context.long_exit==0:
order_target_value(stock, 25000)
context.long_entry = 1
context.long_exit = 0

print 'Long #1, ' + str(stock) + ', '+ str(get_datetime().astimezone(timezone('US/Eastern')))

elif context.long_entry ==1 and context.long_exit==0:
order_target_value(stock, 25000)
context.long_entry = 2
context.long_exit = 0

print 'Long #2, ' + str(stock) + ', '+ str(get_datetime().astimezone(timezone('US/Eastern')))

elif context.long_entry ==2 and context.long_exit==0:
order_target_value(stock, 25000)
context.long_entry = 3
context.long_exit = 0

print 'Long #3, ' + str(stock) + ', '+ str(get_datetime().astimezone(timezone('US/Eastern')))

elif context.long_entry ==3 and context.long_exit==0:
order_target_value(stock, 25000)
context.long_entry = 4
context.long_exit = 0

print 'Long #4, ' + str(stock) + ', '+ str(get_datetime().astimezone(timezone('US/Eastern')))

# Bridge to go back to Long
elif context.long_entry ==0 and context.long_exit==4:
order_target_value(stock, 25000)
context.long_entry = 1
context.long_exit = 0

print 'Long #5, ' + str(stock) + ', '+ str(get_datetime().astimezone(timezone('US/Eastern')))

# Sell
elif context.long_entry ==4 and context.long_exit==0 and context.trading_day_counter == 25:
order_target_percent(stock, 0)
context.long_entry = 3
context.long_exit = 1
print 'Sell #1, ' + str(stock) + ', ' + str(get_datetime().astimezone(timezone('US/Eastern')))

elif context.long_entry ==3 and context.long_exit==1 and context.trading_day_counter == 25:
order_target_percent(stock, 0)
context.long_entry = 2
context.long_exit = 2
print 'Sell #2, ' + str(stock) + ', ' + str(get_datetime().astimezone(timezone('US/Eastern')))

elif context.long_entry ==2 and context.long_exit==2 and context.trading_day_counter == 25:
order_target_percent(stock, 0)
context.long_entry = 1
context.long_exit = 3