Featured Photo Graham Wizardo on Pexels
In fact, this algo trading Python project was inspired by the recent thread by @simply_robo
Indeed, this is all about Altria Group, Inc. (NYSE: MO) – a Dividend King moving beyond smoking.
In this article, the historical data of $MO will be used to backtest and compare trading strategies based on the very popular trend-following Donchian Channel indicator. This indicator can be a useful tool for traders looking to identify potential entry and exit points in the market.
Specifically, our objective is to compare the highest total return when applying the Donchian Channel Breakout vs ‘buy-and-hold’ strategies if the trades were made from 2018 to 2023 for $MO.
Table of Contents
- TradingView
- Exploratory Data Analysis (EDA)
- Donchian Middle Value Crossover Strategy
- Donchian Channel Breakout Strategy
- Summary
- Explore More
TradingView

Technical analysis summary: This gauge displays a real-time technical analysis overview for your selected timeframe. The summary of Altria Group, Inc is based on the most popular technical indicators, such as Moving Averages, Oscillators and Pivots.
1 Month:

Analyst rating:
Based on 19 analysts giving stock ratings for $MO in the past 3 months.

Financial summary of Altria Group, Inc with all the key numbers. The current MO market cap is 85.021B USD. Next Altria Group, Inc earnings date is April 27, the estimation is 1.19 USD.

Altria Group, Inc key financial stats and ratios.

Altria Group, Inc dividends overview. MO dividends are paid quarterly. The last dividend per share was 0.94 USD. As of today, Dividend Yield (TTM)% is 7.76%.

Altria Group, Inc earnings and revenue. MO earnings for the last quarter are 1.18 USD whereas the estimation was 1.17 USD which accounts for 0.64% surprise. Company revenue for the same period amounts to 5.08B USD despite the estimated figure of 5.15B USD. Estimated earnings for the next quarter are 1.19 USD, and revenue is expected to reach 4.92B USD. Also watch annual changes over time to get a bigger picture of MO earnings and revenue dynamics.

Exploratory Data Analysis (EDA)
Let’s set the working directory YOURPATH
import os
os.chdir(‘YOURPATH’)
os. getcwd()
Let’s import the key libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
define the calcDonchianChannels function
def calcDonchianChannels(data: pd.DataFrame, period: int):
data[“upperDon”] = data[“High”].rolling(period).max()
data[“lowerDon”] = data[“Low”].rolling(period).min()
data[“midDon”] = (data[“upperDon”] + data[“lowerDon”]) / 2
return data
and read the input historical data
ticker = “MO”
yfObj = yf.Ticker(ticker)
data = yfObj.history(start=”2018-01-01″, end=”2023-02-22″).drop(
[“Volume”, “Stock Splits”], axis=1)
data = calcDonchianChannels(data, 20)
data.tail()

Let’s plot the stock price and the Donchian Channels
colors = plt.rcParams[“axes.prop_cycle”].by_key()[“color”]
plt.figure(figsize=(12, 8))
plt.plot(data[“Close”], label=”Close”)
plt.plot(data[“upperDon”], label=”Upper”, c=colors[1])
plt.plot(data[“lowerDon”], label=”Lower”, c=colors[4])
plt.plot(data[“midDon”], label=”Mid”, c=colors[2], linestyle=”:”)
plt.fill_between(data.index, data[“upperDon”], data[“lowerDon”], alpha=0.3,
color=colors[6])
plt.xlabel(“Date”)
plt.ylabel(“Price in $”)
plt.title(f”Donchian Channels for {ticker}”)
plt.legend()
plt.show()

Donchian Middle Value Crossover Strategy
Let’s define several functions to calculate returns and summary stats of the trading strategy.
def midDonCrossOver(data: pd.DataFrame, period: int=20, shorts: bool=True):
data = calcDonchianChannels(data, period)
data[“position”] = np.nan
data[“position”] = np.where(data[“Close”]>data[“midDon”], 1,
data[“position”])
if shorts:
data[“position”] = np.where(data[“Close”]<data[“midDon”], -1,
data[“position”])
else:
data[“position”] = np.where(data[“Close”]<data[“midDon”], 0,
data[“position”])
data[“position”] = data[“position”].ffill().fillna(0)
return calcReturns(data)
def calcReturns(df):
df[‘returns’] = df[‘Close’] / df[‘Close’].shift(1)
df[‘log_returns’] = np.log(df[‘returns’])
df[‘strat_returns’] = df[‘position’].shift(1) * df[‘returns’]
df[‘strat_log_returns’] = df[‘position’].shift(1) * \
df[‘log_returns’]
df[‘cum_returns’] = np.exp(df[‘log_returns’].cumsum()) – 1
df[‘strat_cum_returns’] = np.exp(
df[‘strat_log_returns’].cumsum()) – 1
df[‘peak’] = df[‘cum_returns’].cummax()
df[‘strat_peak’] = df[‘strat_cum_returns’].cummax()
return df
def getStratStats(log_returns: pd.Series,
risk_free_rate: float = 0.02):
stats = {} # Total Returns
stats[‘tot_returns’] = np.exp(log_returns.sum()) – 1
# Mean Annual Returns
stats[‘annual_returns’] = np.exp(log_returns.mean() * 252) – 1
# Annual Volatility
stats[‘annual_volatility’] = log_returns.std() * np.sqrt(252)
# Sortino Ratio
annualized_downside = log_returns.loc[log_returns<0].std() * \
np.sqrt(252)
stats[‘sortino_ratio’] = (stats[‘annual_returns’] – \
risk_free_rate) / annualized_downside
# Sharpe Ratio
stats[‘sharpe_ratio’] = (stats[‘annual_returns’] – \
risk_free_rate) / stats[‘annual_volatility’]
# Max Drawdown
cum_returns = log_returns.cumsum() – 1
peak = cum_returns.cummax()
drawdown = peak – cum_returns
max_idx = drawdown.argmax()
stats[‘max_drawdown’] = 1 – np.exp(cum_returns[max_idx]) \
/ np.exp(peak[max_idx])
# Max Drawdown Duration
strat_dd = drawdown[drawdown==0]
strat_dd_diff = strat_dd.index[1:] – strat_dd.index[:-1]
strat_dd_days = strat_dd_diff.map(lambda x: x.days).values
strat_dd_days = np.hstack([strat_dd_days,
(drawdown.index[-1] – strat_dd.index[-1]).days])
stats[‘max_drawdown_duration’] = strat_dd_days.max()
return {k: np.round(v, 4) if type(v) == np.float_ else v
for k, v in stats.items()}
Let’s compare cumulative returns for the Mid Donchian Crossover Strategy vs “Buy-and-Hold”
midDon = midDonCrossOver(data.copy(), 20, shorts=False)
plt.figure(figsize=(12, 4))
plt.plot(midDon[“strat_cum_returns”] * 100, label=”Mid Don X-Over”)
plt.plot(midDon[“cum_returns”] * 100, label=”Buy and Hold”)
plt.title(“Cumulative Returns for Mid Donchian Cross-Over Strategy”)
plt.xlabel(“Date”)
plt.ylabel(“Returns (%)”)
plt.xticks(rotation=45)
plt.legend()
plt.show()
stats = pd.DataFrame(getStratStats(midDon[“log_returns”]),
index=[“Buy and Hold”])
stats = pd.concat([stats,
pd.DataFrame(getStratStats(midDon[“strat_log_returns”]),
index=[“MidDon X-Over”])])
stats

with the summary table

Donchian Channel Breakout Strategy
Let’s look at the Donchian Channel Breakout Strategy
def donChannelBreakout(data, period=20, shorts=True):
data = calcDonchianChannels(data, period)
data[“position”] = np.nan
data[“position”] = np.where(data[“Close”]>data[“upperDon”].shift(1), 1,
data[“position”])
if shorts:
data[“position”] = np.where(
data[“Close”]<data[“lowerDon”].shift(1), -1, data[“position”])
else:
data[“position”] = np.where(
data[“Close”]<data[“lowerDon”].shift(1), 0, data[“position”])
data[“position”] = data[“position”].ffill().fillna(0)
return calcReturns(data)
The corresponding plot is as follows
breakout = donChannelBreakout(data.copy(), 20, shorts=False)
plt.figure(figsize=(12, 4))
plt.plot(breakout[“strat_cum_returns”] * 100, label=”Donchian Breakout”)
plt.plot(breakout[“cum_returns”] * 100, label=”Buy and Hold”)
plt.title(“Cumulative Returns for Donchian Breakout Strategy”)
plt.xlabel(“Date”)
plt.ylabel(“Returns (%)”)
plt.xticks(rotation=45)
plt.legend()
plt.show()
stats = pd.concat([stats,
pd.DataFrame(getStratStats(breakout[“strat_log_returns”]),
index=[“Donchian Breakout”])])
stats

with the summary table

Summary
- We have applied the candlestick-based Donchian channel trading indicator to identify potential breakouts and retracements of the $MO stock.
- We have implemented the two complementary strategies: the Donchian Middle Value Crossover (DXO) Strategy and the Donchian Channel Breakout (DCB) Strategy.
- Comparisons show that ROI(DXO & DCB) >> ROI(‘buy-and-hold’ strategy) for $MO 2018-2023, where ROI = total/annual returns.
- Other KPIs suggest the following:
- Annual volatility (DXO) ~ Annual volatility (DCB) < Annual volatility (‘buy-and-hold’)
2. Sharpe ratio (‘buy-and-hold’) < 0, i.e. the risk-free or benchmark rate is greater than the portfolio’s historical or projected return, or else the portfolio’s return is expected to be negative.
3. Sharpe ratio (DXO) << Sharpe ratio (DCB), i.e. a higher DCB Sharpe ratio indicates relatively good investment performance of DCB, given the risk.
4. Sortino ratio (DXO) << Sortino ratio (DCB), i.e. we go for the DCB strategy with the highest Sortino ratio.
5. A negative Sortino Ratio of ‘buy-and-hold’ suggests that the potential investor may not be rewarded for the risk taken with the investment.
- The return from both strategies can be further maximised by combining the risk-aware trading approach, Return/Risk balanced portfolio optimization and the Donchian strategies into a single framework.
- Results are consistent with the TradingView and @simply_robo technical analysis summary and recommendations.
Explore More
Make a one-time donation
Make a monthly donation
Make a yearly donation
Choose an amount
Or enter a custom amount
Your contribution is appreciated.
Your contribution is appreciated.
Your contribution is appreciated.
DonateDonate monthlyDonate yearly