| Overall Statistics |
|
Total Orders 19 Average Win 13.30% Average Loss -0.45% Compounding Annual Return 201.318% Drawdown 32.100% Expectancy 19.364 Start Equity 100000 End Equity 193242.39 Net Profit 93.242% Sharpe Ratio 2.495 Sortino Ratio 2.633 Probabilistic Sharpe Ratio 72.488% Loss Rate 33% Win Rate 67% Profit-Loss Ratio 29.55 Alpha 1.515 Beta 0.752 Annual Standard Deviation 0.592 Annual Variance 0.351 Information Ratio 2.651 Tracking Error 0.576 Treynor Ratio 1.966 Total Fees $70.25 Estimated Strategy Capacity $3300000.00 Lowest Capacity Asset DFGR Y44B2353KOKL Portfolio Turnover 1.58% |
#region imports
from AlgorithmImports import *
from typing import List, Dict
from pandas.core.frame import DataFrame
from pandas.core.series import Series
from io import StringIO
#endregion
def get_ranked_data(algo: QCAlgorithm, diff_threshold: float) -> DataFrame:
# source: https://polymarket.com/event/presidential-election-winner-2024?tid=1741681510458
load: str = algo.download(f'data.quantpedia.com/backtesting_data/economic/us_elections_2024.csv')
df: DataFrame = pd.read_csv(StringIO(load), delimiter=';')
# df['date'] = pd.to_datetime(df['date']).dt.date
df['date'] = pd.to_datetime(df['date']) + pd.Timedelta(days=1)
df['date'] = df['date'].dt.date
df.set_index('date', inplace=True)
def assign_ranks(row) -> Series:
if abs(row['Donald Trump'] - row['Kamala Harris']) > diff_threshold:
if row['Donald Trump'] > row['Kamala Harris']:
return pd.Series({'Trump_rank': 1, 'Harris_rank': 0})
else:
return pd.Series({'Trump_rank': 0, 'Harris_rank': 1})
else:
return pd.Series({'Trump_rank': 0, 'Harris_rank': 0})
ranks: DataFrame = df.apply(assign_ranks, axis=1)
return pd.concat([df, ranks], axis=1)
# https://quantpedia.com/strategies/election-driven-arbitrage-strategy/
#
# The investment universe for this strategy includes stocks, currencies, and digital assets that align with the policy agendas of the 2024 U.S. Presidential candidates,
# Donald Trump and Kamala Harris. The initial universe consists of over 1,000 assets, which are filtered down to approximately 300 based on thematic baskets tailored to
# each candidate’s policies. For Trump, the focus is on sectors such as energy, defense, technology, and onshoring. For Harris, the emphasis is on renewable energy,
# healthcare, and fiscal stimulus beneficiaries. The final selection involves approximately 40 assets for each candidate’s long-only portfolio, further narrowed down to
# the top 15 best-performing assets.
# The strategy utilizes prediction market data to inform trading decisions, specifically focusing on daily changes in election outcome probabilities. Critical events are
# defined by daily changes in prediction market odds exceeding a specified threshold (e.g., 4%). For positive events, where odds increase for a candidate, assets that
# exceed a dynamically calculated positive threshold are selected for long positions. For negative events, where odds decrease, assets that remain above a dynamically
# calculated negative floor are selected. The methodology involves calculating dynamic thresholds based on Year-to-Date (YTD) performance, allowing for asset-specific
# sensitivity to market conditions.
# Portfolios are constructed with a focus on long-only positions. Each portfolio consists of the top 15 assets selected based on their alignment with candidate policies
# and historical performance. Rebalancing occurs periodically to reflect changes in prediction market odds and policy developments.
# region imports
from AlgorithmImports import *
import data_tools
from typing import List
from pandas.core.frame import DataFrame
# endregion
class ElectionDrivenArbitrageStrategy(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 10, 1)
self.set_cash(100_000)
self._top_count: int = 5
diff_threshold: float = .04
trade_start_date: datetime = datetime(2024, 11, 1)
trade_end_date: datetime = datetime(2025, 1, 20)
self._election_ranks: DataFrame = data_tools.get_ranked_data(self, diff_threshold)
# source: https://etfdb.com/etfs/industry/
etf_tickers: List[str] = [
'QQQ', 'XLF', 'VNQ', 'XLV', 'SMH', 'XLE', 'XLI', 'XLY', 'XLP', 'XLU',
'XLC', 'GDX', 'KWEB', 'PAVE', 'AMLP', 'IBB', 'ITA', 'IGV', 'XLB', 'CIBR',
'GUNR', 'VOX', 'KBWB', 'IHI', 'PHO', 'KRE', 'ITB', 'SKYY', 'FAS', 'URA',
'TSLL', 'MLPX', 'ICLN', 'COPX', 'XME', 'SIL', 'GRID', 'EIPI', 'KIE', 'PPH',
'BIZD', 'IYC', 'IAI', 'OIH', 'IYK', 'CONL', 'XRT', 'NLR', 'LIT', 'DFGR',
]
self._universe: List[Symbol] = [
self.add_equity(etf_ticker, Resolution.DAILY).symbol for etf_ticker in etf_tickers
]
self.settings.daily_precise_end_time = False
self.settings.minimum_order_margin_portfolio_percentage = 0.
self._trade_flag: bool = False
self.schedule.on(
self.date_rules.on(trade_start_date),
self.time_rules.before_market_close(self._universe[0]),
self._trade
)
self.schedule.on(
self.date_rules.on(trade_end_date),
self.time_rules.at(0, 0),
lambda: self.liquidate()
)
def on_data(self, slice: Slice) -> None:
if not self._trade_flag:
return
self._trade_flag = False
# Get ETFs performance history.
history: DataFrame = self.history(self._universe, start=self._election_ranks.index[0], end=self._election_ranks.index[-1]).unstack(level=0)
if len(history.close.columns) != len(self._universe):
self.log('Not enough data for further calculation.')
return
returns: DataFrame = history.close.pct_change().dropna()
df_combined: DataFrame = pd.merge(self._election_ranks, returns, left_index=True, right_index=True, how='inner')
trump_portfolio: List[Symbol] = list(df_combined[df_combined['Trump_rank'] == 1].iloc[:, 4:].sum().sort_values(ascending=False)[:self._top_count].index)
harris_portfolio: List[Symbol] = list(df_combined[df_combined['Harris_rank'] == 1].iloc[:, 4:].sum().sort_values(ascending=False)[:self._top_count].index)
# Trade execution.
for i, portfolio in enumerate([trump_portfolio, harris_portfolio]):
for symbol in portfolio:
if slice.contains_key(symbol) and slice[symbol]:
self.market_order(symbol, self.calculate_order_quantity(symbol, 1 / len(portfolio)))
def _trade(self) -> None:
self._trade_flag = True