| Overall Statistics |
|
Total Orders 668 Average Win 0.09% Average Loss -0.05% Compounding Annual Return 2.886% Drawdown 14.500% Expectancy 0.753 Start Equity 100000 End Equity 115295.34 Net Profit 15.295% Sharpe Ratio -0.242 Sortino Ratio -0.247 Probabilistic Sharpe Ratio 3.752% Loss Rate 36% Win Rate 64% Profit-Loss Ratio 1.72 Alpha -0.041 Beta 0.358 Annual Standard Deviation 0.064 Annual Variance 0.004 Information Ratio -0.866 Tracking Error 0.099 Treynor Ratio -0.043 Total Fees $668.00 Estimated Strategy Capacity $13000000.00 Lowest Capacity Asset ALD R735QTJ8XC9X Portfolio Turnover 0.37% Drawdown Recovery 1463 |
from AlgorithmImports import *
from scipy.optimize import curve_fit
from scipy.stats import linregress
class SmartInsiderCorporateBuybacksAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(self.end_date - timedelta(5*365))
self.set_cash(100_000)
self.set_warm_up(252 * 5, Resolution.DAILY)
self._min_ic = 0.05
self._min_expected_return = 0.005
self.set_portfolio_construction(InsightWeightingPortfolioConstructionModel(timedelta(days=252)))
# Equities from research: MMM, GS, HON, JNJ, MSFT, TRV, V
tickers = ["MMM", "GS", "HON", "JNJ", "MSFT", "TRV", "V"]
self._equities = []
self._equity_by_buyback_symbol = {}
# Attach session tracking, buyback symbol, and buyback data storage to each security via duck typing
for ticker in tickers:
equity = self.add_equity(ticker)
equity.session.size = 252 * 5 # Track 5 years of daily sessions
buyback_symbol = self.add_data(SmartInsiderTransaction, equity).symbol
equity.buyback_data = {} # Store buyback data keyed by date
self._equities.append(equity)
self._equity_by_buyback_symbol[buyback_symbol] = equity
# Add a Scheduled Event to rebalance the portfolio.
self.schedule.on(
self.date_rules.every_day("MMM"),
self.time_rules.at(10, 0),
self._rebalance
)
def on_data(self, data):
"""Process incoming transaction data and store it."""
for dataset_symbol, transaction in data.get(SmartInsiderTransaction).items():
equity = self._equity_by_buyback_symbol[dataset_symbol]
transaction_date = transaction.end_time.date()
buyback_pct = transaction.usd_value / transaction.usd_market_cap
# Aggregate buybacks by date.
if transaction_date in equity.buyback_data:
equity.buyback_data[transaction_date] += buyback_pct
else:
equity.buyback_data[transaction_date] = buyback_pct
def _rebalance(self):
"""Check entry signals and emit insights."""
if self.is_warming_up:
return
insights = []
for equity in self._equities:
# Get processed data for IC and expectation calculation, plus latest buyback.
result = self._get_data(equity)
if not result:
continue
df, latest_buyback = result
# Calculate the information coefficient.
ic = df.corr().values[0, 1]
if ic < self._min_ic:
continue
# Calculate expected return using latest buyback transaction.
expectation = self._get_expectation(df, latest_buyback)
if expectation <= self._min_expected_return:
continue
# Emit insight for entry.
insights.append(
Insight.price(equity, timedelta(252), InsightDirection.UP, weight=expectation*ic)
)
if insights:
self.emit_insights(insights)
def _get_data(self, equity):
"""Get processed dataframe for IC and expectation calculation.
Args:
equity: The equity Security to get the processed dataframe for.
Returns:
Tuple of (DataFrame with 1-year forward returns and buyback percentages, latest buyback percentage),
or None if insufficient data.
"""
if not equity.buyback_data:
return
# Calculate 1-year forward returns.
history = pd.Series(
[x.close for x in equity.session],
index=[x.end_time.date() for x in equity.session]
).sort_index().pct_change(252).shift(-252).dropna()
# Convert stored buyback data to a Series.
buyback_aggregated = pd.Series(equity.buyback_data).sort_index()
# Extract latest buyback percentage for expectation calculation.
latest_buyback = np.log(buyback_aggregated.iloc[-1])
# Concatenate dataframes keeping only rows with data
df = pd.concat([history, np.log(buyback_aggregated)], axis=1).replace([np.inf, -np.inf], np.nan).dropna()
if df.empty:
return
return (df, latest_buyback)
def _exponential_curve(self, x, a, b, c):
"""Exponential decay curve function for buyback analysis."""
return a * np.exp(-b * x) + c
def _get_expectation(self, df, transaction):
"""Calculate expected return given buyback percentage.
Args:
df: DataFrame with historical data for curve fitting.
transaction: Current buyback percentage (log-scaled).
Returns:
Expected return percentage, or -1 if curve fitting fails.
"""
x = df.iloc[:, 1].values
y = df.iloc[:, 0].values
# Try exponential curve fitting.
try:
popt, _ = curve_fit(
self._exponential_curve,
x, y,
p0=[np.mean(y), 1.0, 0.0],
bounds=([-np.inf, 0, -np.inf], [np.inf, np.inf, np.inf]),
maxfev=10_000
)
return self._exponential_curve(transaction, *popt)
except:
pass
# Fallback to simple linear regression.
try:
slope, intercept, _, _, _ = linregress(x, y)
return slope * transaction + intercept
except:
return -1