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}")