68d9f48c2d
- Adding new `CompositeAlphaModel.AddAlphaModel()` - Adding C#/Py regression algorithms using the new `QCAlgorith.AddAlphaModel()` - Improving exception message - Add python version of `QCAlgorith.AddAlphaModel()`
219 lines
10 KiB
Python
219 lines
10 KiB
Python
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||
#
|
||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software
|
||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License.
|
||
|
||
''' Contingent Claim Analysis is put forth by Robert Merton, recepient of the Noble Prize in Economics in 1997 for his work in contributing to
|
||
Black-Scholes option pricing theory, which says that the equity market value of stockholders’ equity is given by the Black-Scholes solution
|
||
for a European call option. This equation takes into account Debt, which in CCA is the equivalent to a strike price in the BS solution. The probability
|
||
of default on corporate debt can be calculated as the N(-d2) term, where d2 is a function of the interest rate on debt(µ), face value of the debt (B), value of the firm's assets (V),
|
||
standard deviation of the change in a firm's asset value (σ), the dividend and interest payouts due (D), and the time to maturity of the firm's debt(τ). N(*) is the cumulative
|
||
distribution function of a standard normal distribution, and calculating N(-d2) gives us the probability of the firm's assets being worth less
|
||
than the debt of the company at the time that the debt reaches maturity -- that is, the firm doesn't have enough in assets to pay off its debt and defaults.
|
||
|
||
We use a Fine/Coarse Universe Selection model to select small cap stocks, who we postulate are more likely to default
|
||
on debt in general than blue-chip companies, and extract Fundamental data to plug into the CCA formula.
|
||
This Alpha emits insights based on whether or not a company is likely to default given its probability of default vs a default probability threshold that we set arbitrarily.
|
||
|
||
|
||
Prob. default (on principal B at maturity T) = Prob(VT < B) = 1 - N(d2) = N(-d2) where -d2(µ) = -{ln(V/B) + [(µ - D) - ½σ2]τ}/ σ √τ.
|
||
N(d) = (univariate) cumulative standard normal distribution function (from -inf to d)
|
||
B = face value (principal) of the debt
|
||
D = dividend + interest payout
|
||
V = value of firm’s assets
|
||
σ (sigma) = standard deviation of firm value changes (returns in V)
|
||
τ (tau) = time to debt’s maturity
|
||
µ (mu) = interest rate
|
||
|
||
This alpha is part of the Benchmark Alpha Series created by QuantConnect which are open
|
||
sourced so the community and client funds can see an example of an alpha.
|
||
'''
|
||
|
||
from clr import AddReference
|
||
AddReference("QuantConnect.Algorithm")
|
||
|
||
import scipy.stats as sp
|
||
import pandas as pd
|
||
import numpy as np
|
||
from datetime import datetime, timedelta
|
||
|
||
from QuantConnect import *
|
||
from QuantConnect.Algorithm import *
|
||
from QuantConnect.Algorithm.Framework.Selection import *
|
||
from Risk.NullRiskManagementModel import NullRiskManagementModel
|
||
from Portfolio.EqualWeightingPortfolioConstructionModel import EqualWeightingPortfolioConstructionModel
|
||
from Execution.ImmediateExecutionModel import ImmediateExecutionModel
|
||
|
||
|
||
class ContingentClaimsAnalysisDefaultPredictionAlpha(QCAlgorithm):
|
||
|
||
def Initialize(self):
|
||
|
||
## Set requested data resolution and variables to help with Universe Selection control
|
||
self.UniverseSettings.Resolution = Resolution.Daily
|
||
self.month = None
|
||
self.symbols = None
|
||
|
||
## Declare single variable to be passed in multiple places -- prevents issue with conflicting start dates declared in different places
|
||
self.SetStartDate(2018,1,1)
|
||
self.SetCash(100000)
|
||
|
||
## SPDR Small Cap ETF is a better benchmark than the default SP500
|
||
self.SetBenchmark('IJR')
|
||
|
||
## Set Universe Selection Model
|
||
self.SetUniverseSelection(FineFundamentalUniverseSelectionModel(self.CoarseSelectionFunction, self.FineSelectionFunction, None, None))
|
||
self.SetSecurityInitializer(lambda security: security.SetFeeModel(ConstantFeeModel(0)))
|
||
## Set CCA Alpha Model
|
||
self.SetAlpha(ContingentClaimsAnalysisAlphaModel())
|
||
|
||
## Set Portfolio Construction Model
|
||
self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
|
||
|
||
## Set Execution Model
|
||
self.SetExecution(ImmediateExecutionModel())
|
||
|
||
## Set Risk Management Model
|
||
self.SetRiskManagement(NullRiskManagementModel())
|
||
|
||
|
||
def CoarseSelectionFunction(self, coarse):
|
||
## Boolean controls so that our symbol universe is only updated once per month
|
||
if self.Time.month == self.month:
|
||
return self.symbols
|
||
else:
|
||
self.month = self.Time.month
|
||
|
||
## Sort by dollar volume, lowest to highest
|
||
sortedByDollarVolume = sorted([x for x in coarse if x.HasFundamentalData], key=lambda x: x.DollarVolume, reverse=True)
|
||
|
||
## Filter for assets with fundamental data
|
||
filtered = [ x.Symbol for x in sortedByDollarVolume ]
|
||
|
||
## Return smallest 750 -- idea is that smaller companies are most likely to go bankrupt than blue-chip companies
|
||
self.symbols = filtered[:750]
|
||
|
||
return self.symbols
|
||
|
||
|
||
def FineSelectionFunction(self, fine):
|
||
## Boolean controls so that our symbol universe is updated only at the beginning of each month
|
||
if self.Time.month == self.month:
|
||
return self.symbols
|
||
else:
|
||
self.month = self.Time.month
|
||
## Select symbols with data necessary for our pricing model
|
||
|
||
def IsValid(x):
|
||
statement = x.FinancialStatements
|
||
sheet = statement.BalanceSheet
|
||
total_assets = sheet.TotalAssets
|
||
ratios = x.OperationRatios
|
||
|
||
return total_assets.OneMonth > 0 and \
|
||
total_assets.ThreeMonths > 0 and \
|
||
total_assets.SixMonths > 0 and \
|
||
total_assets.TwelveMonths > 0 and \
|
||
sheet.CurrentLiabilities.TwelveMonths > 0 and \
|
||
sheet.InterestPayable.TwelveMonths > 0 and \
|
||
ratios .TotalAssetsGrowth.OneYear > 0 and \
|
||
statement.IncomeStatement.GrossDividendPayment.TwelveMonths > 0 and \
|
||
ratios.ROA.OneYear > 0
|
||
|
||
fineFilter = sorted(fine, key=lambda x: IsValid(x))
|
||
|
||
if len(fine) == len(fineFilter):
|
||
self.Debug("Did not filter")
|
||
|
||
self.symbols = [ x.Symbol for x in fineFilter]
|
||
|
||
return self.symbols
|
||
|
||
|
||
class ContingentClaimsAnalysisAlphaModel:
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
self.symbolDataBySymbol = {}
|
||
self.month = None
|
||
self.default_threshold = kwargs['default_threshold'] if 'default_threshold' in kwargs else 0.25
|
||
|
||
def Update(self, algorithm, data):
|
||
|
||
'''Updates this alpha model with the latest data from the algorithm.
|
||
This is called each time the algorithm receives data for subscribed securities
|
||
Args:
|
||
algorithm: The algorithm instance
|
||
data: The new data available
|
||
Returns:
|
||
The new insights generated'''
|
||
|
||
## Build a list to hold our insights
|
||
insights = []
|
||
|
||
for symbol, symbolData in self.symbolDataBySymbol.items():
|
||
pod = symbolData.ProbabilityOfDefault
|
||
|
||
## If Prob. of Default is greater than our set threshold, then emit an insight indicating that this asset is trending downward
|
||
if (pod >= self.default_threshold) and (pod != 1.0):
|
||
insights.append(Insight(symbol, timedelta(days = 30), InsightType.Price, InsightDirection.Down, pod, None))
|
||
algorithm.Log(str(symbol) + 'Probability of Default: ' + str(round(pod*100,4)) + '%')
|
||
|
||
return insights
|
||
|
||
def OnSecuritiesChanged(self, algorithm, changes):
|
||
|
||
for removed in changes.RemovedSecurities:
|
||
algorithm.Log('Removed: ' + str(removed.Symbol))
|
||
symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None)
|
||
|
||
# initialize data for added securities
|
||
symbols = [ x.Symbol for x in changes.AddedSecurities ]
|
||
|
||
for symbol in symbols:
|
||
if symbol not in self.symbolDataBySymbol:
|
||
|
||
## Retrieve fundamentals data necessary for our CCA valuation
|
||
security = algorithm.Securities[symbol]
|
||
if security.Fundamentals is None or security.Fundamentals.FinancialStatements is None or security.Fundamentals.OperationRatios is None:
|
||
continue
|
||
symbolData = SymbolData(security)
|
||
self.symbolDataBySymbol[symbol] = symbolData
|
||
|
||
class SymbolData:
|
||
|
||
def __init__(self, security):
|
||
statement = security.Fundamentals.FinancialStatements
|
||
sheet = statement.BalanceSheet
|
||
total_assets = sheet.TotalAssets
|
||
|
||
self.tau = 360 ## Days
|
||
self.Symbol = security.Symbol
|
||
self.mu = security.Fundamentals.OperationRatios.ROA.OneYear
|
||
self.V = total_assets.TwelveMonths
|
||
self.B = sheet.CurrentLiabilities.TwelveMonths
|
||
self.D = statement.IncomeStatement.GrossDividendPayment.TwelveMonths + sheet.InterestPayable.TwelveMonths
|
||
|
||
series = pd.Series(
|
||
[
|
||
total_assets.OneMonth,
|
||
total_assets.ThreeMonths,
|
||
total_assets.SixMonths,
|
||
self.V
|
||
])
|
||
sigma = series.iloc[series.nonzero()[0]]
|
||
self.sigma = np.std(sigma.pct_change()[1:len(sigma)])
|
||
|
||
## This model applies options pricing theory, Black-Scholes specifically, to fundamental data
|
||
## to give the probability of a default
|
||
@property
|
||
def ProbabilityOfDefault(self):
|
||
d2 = ((np.log(self.V) - np.log(self.B)) + ((self.mu - self.D) - 0.5*self.sigma**2.0)*self.tau)/ (self.sigma*np.sqrt(self.tau))
|
||
return sp.norm.cdf(-d2) |