| Overall Statistics |
|
Total Trades 1667 Average Win 0% Average Loss 0% Compounding Annual Return 0.646% Drawdown 0.300% Expectancy 0 Net Profit 0.642% Sharpe Ratio 1.206 Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha -0.003 Beta 0.451 Annual Standard Deviation 0.005 Annual Variance 0 Information Ratio -2.519 Tracking Error 0.005 Treynor Ratio 0.014 Total Fees $1667.00 |
import json
import random
import datetime
import numpy as np
import decimal as d
from collections import defaultdict
from collections import OrderedDict
import QuantConnect
from QuantConnect import Time
from QuantConnect import Orders
from QuantConnect.Data.UniverseSelection import *
earnings_url = 'http://198.199.107.121/earnings.json'
class Earnings:
"""
Helper class for accessing earnings data.
"""
def __init__(self, json_data):
earnings_items = sorted(json_data.items())
self.datemap = OrderedDict(earnings_items)
all_tickers = []
for date, ticker_list in self.datemap.items():
all_tickers += ticker_list
self.all_tickers = sorted(set(all_tickers))
def _check_date(self, iso_date):
""" Make sure the date was passed correctly """
year, month, day = iso_date.split('-')
if len(year) != 4:
raise ValueError(f"{iso_date}: Improperly formatted year: {year}")
if len(month) != 2:
raise ValueError(f"{iso_date}: Improperly formatted month: {month}")
if len(day) != 2:
raise ValueError(f"{iso_date}: Improperly formatted day: {day}")
def next_date_after(self, iso_date):
""" Find the first date in our historical earnings dates
after the passed date, on which some company reported.
"""
self._check_date(iso_date)
for date_key in self.datemap.keys():
if date_key > iso_date: break
else:
raise KeyError(f"No known earnings date after {iso_date}")
return date_key
def next_date_before(self, iso_date):
""" Find the first date in our historical earnings dates
before the passed date, on which some company reported.
"""
self._check_date(iso_date)
for date_key in reversed(self.datemap.keys()):
if date_key < iso_date: break
else:
raise KeyError(f"No known earnings date before {iso_date}")
return date_key
def next_companies_after(self, iso_date):
""" Find the set of companies that will be reporting on the
first date in our catalogue of historical earnings dates
after the passed date.
"""
next_date = self.next_date_after(iso_date)
return self.datemap[next_date]
def next_companies_before(self, iso_date):
""" Find the set of companies that will be reporting on the
first date in our catalogue of historical earnings dates
before the passed date.
"""
prev_date = self.next_date_before(iso_date)
return self.datemap[prev_date]
def on(self, iso_date):
""" Returns a list of companies reporting on a given date. """
self._check_date(iso_date)
try:
return self.datemap[iso_date]
except KeyError:
return []
class Algorithm(QCAlgorithm):
def Initialize(self):
"""
This class contains the main code for user story ch368.
In the example that follows, we download a json file
of historical earnings dates and use it to select the
"universe" of stocks that we're interested in each day.
"""
self.SetStartDate(2017, 1, 1)
self.SetEndDate(2017, 12, 31)
self.SetCash(1_000_000)
self.openLimitOrders = defaultdict(list)
self.currentUniverse = []
# The following set contains the symbols that
# QuantConnect complains about if we try to buy
# them, with an error message simply saying that
# they have been marked as "non-tradeable" without
# providing any clear reason. We can just skip them.
self.blacklistedTickers = {"BGC", "MON", "TWX"}
earnings_data = self.Download(earnings_url)
self.earnings = Earnings(json.loads(earnings_data))
for ticker in self.earnings.all_tickers:
self.AddEquity(ticker, Resolution.Minute)
# SPY is used as a reference symbol for scheduling events,
# following the Quantconnect documentation and examples here:
# https://www.quantconnect.com/docs/algorithm-reference/scheduled-events
self.AddEquity("SPY", Resolution.Minute)
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.AfterMarketOpen("SPY", 1),
self.marketOpenCallback
)
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 1),
self.marketCloseCallback
)
self.UniverseSettings.Resolution = Resolution.Minute
self.AddUniverse("earnings-universe", self.universeSelector)
def universeSelector(self, dt):
today = dt.date().isoformat()
earningsToday = self.earnings.on(today)
self.currentUniverse = [t for t in earningsToday if t not in self.blacklistedTickers]
self.Debug(f"Universe for {today} is {self.currentUniverse}")
# returning a non null value here is required by the framework,
# even though we can access the same data most directly simply
# by referring to the attribute we just set on self.
return self.currentUniverse
def OnData(self, data):
"""
This method is normally called for each timestep
of the data we have subscribed to. For example, if
we called self.AddEquity('AAPL', Resolution.Minute)
in the Initialize method, then the OnData method will
be called every minute, and similarly for any other
resolution we might have subscribed to. In this
example, we will not have any reason to call this
method, but the framework requires us to include it.
"""
pass
def getPrice(self, symbol):
price = float(self.Securities[symbol].Price)
return price
def marketOpenCallback(self):
"""
Scheduled to be called soon after market open.
For this example, we'll buy 1 share of each company
that reported earnings today if its stock price drops
by two percent at any point within the following 2 days.
If no such relative price drop occurs after the end of
the 2nd day, the limit order is cancelled.
"""
shares = 1
fraction = 0.02
lifetime = 2
for ticker in self.currentUniverse:
try:
price = self.getPrice(ticker)
except:
#self.Debug(f"Getting price failed for ticker '{ticker}'")
continue
limitPrice = price * (1 - fraction)
try:
order = self.LimitOrder(ticker, shares, limitPrice)
order.daysUntilExpiry = lifetime
self.openLimitOrders[ticker].append(order)
#self.Debug(f"marketOpenCallback: Placed order to buy {shares} shares of {ticker} at {limitPrice}")
except:
pass
#self.Debug(f"marketOpenCallback: Placing limit order for {ticker} failed.")
def marketCloseCallback(self):
"""
Scheduled to be called soon before market close.
Here we determine which of our orders have filled,
and which of our orders have expired. If a given
order has neither filled nor expired, we decrement
its number of days until expiry by 1, and keep it
around for the following trading day.
"""
newOpenLimitOrders = defaultdict(list)
for ticker, openOrders in self.openLimitOrders.items():
for order in openOrders:
if order.Status == OrderStatus.Filled:
#self.Debug(f"marketCloseCallback: Order for {ticker} was filled")
continue
if order.daysUntilExpiry <= 0:
try:
order.Cancel(f"marketCloseCallback: Order for {ticker} expired")
except:
orderType = self.decodeEventType(order)
#self.Debug(f"Failed to cancel order of type {orderType}")
continue
order.daysUntilExpiry -= 1
newOpenLimitOrders[ticker].append(order)
self.openLimitOrders = newOpenLimitOrders
def decodeEventType(self, orderEvent):
"""
Turns the numeric values we receive from
orderEvent.Status into a human readable string so we
can tell what kind of event is being passed when the
OnOrderEvent method is automatically called on fills
"""
statuses = ('New', 'Submitted', 'PartiallyFilled', 'Filled',
'Canceled', 'None', 'Invalid', 'CancelPending')
order_statuses = {
getattr(Orders.OrderStatus, status): status
for status in statuses
}
return order_statuses[orderEvent.Status]
def OnOrderEvent(self, orderEvent):
"""
This method is called by the QuantConnect framework whenever
an "order event" occurs. Common events occur at the moment
when an order is submitted, fully filled, partially filled,
cancelled, or at the moment cancellation is requested, but
before the cancellation occurs. For our purposes, the most
relevant event types are the ones that are emitted when a
limit order either (a) fills or (b) is canceled.
"""
symbol = orderEvent.Symbol
filled = orderEvent.FillQuantity
eventType = self.decodeEventType(orderEvent)
order = self.Transactions.GetOrderById(orderEvent.OrderId)
createdTime = order.CreatedTime
if eventType == 'Canceled':
pass
# self.Debug(f"OnOrderEvent: Cancelling order for symbol {symbol} created at time {createdTime}")
elif eventType == 'Submitted':
# These "submitted" events mostly just end up adding noise to the logfile.
# We know when we submit a LimitOrder, and these events occur as
# deterministically as we could hope for after each such order that
# we place, so any logging we need to do concerning when an order is
# submitted can just be done at the moment we submit it.
pass
else:
pass
#self.Debug(f"OnOrderEvent: Got '{eventType}' orderEvent for {symbol}, fill quantity is {filled}")