Traders' Tip text
To my taste it's pleasant when a trading method makes sense to the extent that it can be explained in layman's terms. In September 2019 Traders' Tips, Perry J Kaufman presents exactly such simple technique applicable to any instrument that demonstrates seasonal patterns. Wealth-Lab lets traders implement the seasonal trading system with additional visualization logic.
Below you can examine the strategy code in C# which implements the trading system based on the frequency of positive returns. To make its output more visual, let's also plot the frequency of positive returns by month. We'll plot it as a histogram on-the-fly using the power provided by .NET framework. Its charting engine extends Wealth-Lab's potential of plotting various objects, images and text.
Figure 1. The histogram that overlays the chart of USO (U.S. Oil Fund ETF) shows the frequency of positive returns by month.
The oil ETF is a good choice here as it clearly demonstrates some recent (rolling 4 years) seasonality pattern of increasing frequency of positive returns from May to July, and the opposite in November. There’s many factors to it, let’s mention just some of them. Vehicle sales tend to drop in the winter (and demand for gasoline with them), oil is known to rise during the heating season, Summer's air conditioning spurs power generation which again uses oil and so on.
WealthScript Code (C#)
Since it has some prerequisites, it’s preferred that you simply download this code in Wealth-Lab (hit Ctrl-O and choose "Download...") rather than copy/paste it:
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using WealthLab;
using WealthLab.Indicators;
using System.Linq; //Click "References...", check "System.Core", click OK
using System.IO;
using System.Globalization;
using System.Windows.Forms.DataVisualization.Charting;
//1. Click "References...", then "Other Assemblies..." > "Add a reference"
//2. In "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\" (or "Framework" on 32-bit systems),
//choose and okay "System.Windows.Forms.DataVisualization.dll"
namespace WealthLab.Strategies
{
public class SC_2019_09_KaufmanSeasonality : WealthScript
{
private StrategyParameter paramYears;
private StrategyParameter paramPrice;
private StrategyParameter paramThresholdHigh;
private StrategyParameter paramThresholdLow;
public SC_2019_09_KaufmanSeasonality()
{
paramYears = CreateParameter("Average, Years", 4, 2, 100, 1);
paramPrice = CreateParameter("Use (H+L)/2 or Close", 0, 0, 1, 1);
paramThresholdHigh = CreateParameter("High freq >", 75, 50, 90, 5);
paramThresholdLow = CreateParameter("Low freq <", 25, 5, 50, 5);
}
protected override void Execute()
{
if (Bars.Scale != BarScale.Monthly)
{
DrawLabel( PricePane, "Please switch to Monthly scale");
Abort();
}
var howManyYearsToAverage = paramYears.ValueInt;
var firstYearWithValidData = Date[0].Year + howManyYearsToAverage;
var startBar = DateTimeToBar(new DateTime( firstYearWithValidData, 12, 31), false);
//3. Only trade if the high frequency is 75 % or greater and the low frequency is 25 % or lower.
var thresholdHigh = paramThresholdHigh.ValueInt / 100d;
var thresholdLow = paramThresholdLow.ValueInt / 100d;
//Average annual price
DataSeries avgYearlyPrice = AveragePrice.Series(BarScaleConverter.ToYearly(Bars));
//Average monthly prices (take AveragePrice or simply Close)
SetScaleMonthly();
DataSeries avgMonthlyPrice = paramPrice.ValueInt == 0 ? AveragePrice.Series(Bars) : Close;
RestoreScale();
avgMonthlyPrice = Synchronize( avgMonthlyPrice);
avgYearlyPrice = Synchronize( avgYearlyPrice);
HideVolume();
//Collect monthly average price
var lstMonths = new List<MonthData>();
for (int bar = 1; bar < Bars.Count; bar++)
{
if (Date[bar].Month != Date[bar - 1].Month) //New month
{
lstMonths.Add(new MonthData(Date[bar].Month, Date[bar].Year, avgMonthlyPrice[bar]));
}
}
//Calculations
for (int bar = GetTradingLoopStartBar( startBar); bar < Bars.Count; bar++)
{
if (bar <= 0)
continue;
int yearTo = Date[bar].Year;
int yearFrom = yearTo - 1 - howManyYearsToAverage;
//Average price by year
var yearlyAverages = lstMonths.GroupBy(i => i.Year)
.Where(i => i.Key < yearTo & i.Key >= yearFrom)
.Select(g => new { Year = g.Key, Average = g.Average(a => a.AvgPrice) });
var lstFreqUp = new Dictionary<int, double>();
var lstBreakdown = new Dictionary<int, Tuple<double, double>>();
//Calculate Monthly Adjusted Returns, Up Months and Frequency of Positive Returns
for (int month = 1; month <= 12; month++)
{
int monthCount = 0, upMonths = 0;
double freqUp = 0d, monthlyAdjReturn = 0d;
foreach (var _m in lstMonths)
{
//Ensure this year's data is excluded from processing and trading
if (_m.Month == month && _m.Year < yearTo && _m.Year >= yearFrom)
{
monthCount++;
var givenYearAverage = yearlyAverages.GroupBy(i => i.Year).
Where(i => i.Key == _m.Year).First().ToList();
var _adjReturn = _m.AvgPrice / givenYearAverage[0].Average - 1;
if (_adjReturn > 0)
upMonths++;
monthlyAdjReturn += _adjReturn;
}
}
if (monthCount > 0)
{
freqUp = upMonths / (double)monthCount;
monthlyAdjReturn /= monthCount;
}
//1. Average the monthly frequency of the past N years.
lstFreqUp.Add(month, freqUp);
lstBreakdown.Add(month, new Tuple<double, double>(freqUp, monthlyAdjReturn));
}
//Plot actual chart of Frequency of Positive Returns (for last N years)
if(bar == Bars.Count-1)
{
Chart chart = new Chart(); //Histogram chart
chart.BackColor = Color.Transparent;
chart.Width = 500;
string name = "Bins";
chart.ChartAreas.Add(name);
chart.ChartAreas[0].AxisX.MajorGrid.Enabled = chart.ChartAreas[0].AxisY.MajorGrid.Enabled = false;
chart.ChartAreas[0].AxisX.Title = "Month"; //Custom axis titles
chart.ChartAreas[0].AxisY.Title = "Frequency of Positive Returns";
chart.ChartAreas[0].BackColor = Color.Transparent;
chart.ChartAreas[0].AxisY.Minimum = 0; chart.ChartAreas[0].AxisY.Maximum = 100;
chart.Series.Add(name);
chart.Series[name].ChartType = SeriesChartType.Column;
foreach (var f in lstFreqUp)
{
chart.Series[name].Points.AddXY(
CultureInfo.GetCultureInfo("en-US").DateTimeFormat.GetMonthName(f.Key), f.Value * 100d);
}
using (MemoryStream memStream = new MemoryStream()) {
chart.SaveImage(memStream, ChartImageFormat.Png);
Image img = Image.FromStream(memStream);
DrawImage( PricePane, img, Bars.Count-50, Close[Bars.Count-50], false );
}
}
//Trading
if( IsLastPositionActive)
{
int monthToExit = (int)LastPosition.Tag;
if (Date[bar].Month == monthToExit)
ExitAtClose(bar, LastPosition, monthToExit.ToString());
}
else
{
//Month numbers with frequency higher (lower) than a threshold
var highFreqMonths = lstFreqUp.Where(p => p.Value > thresholdHigh);
var lowFreqMonths = lstFreqUp.Where(p => p.Value < thresholdLow);
var resultsAreValid = (highFreqMonths.Count() > 0 && lowFreqMonths.Count() > 0);
if (resultsAreValid)
{
//2. Find the last occurrences of the highest (lowest) frequency
int lastHighestFrequencyMonth = 0, lastLowestFrequencyMonth = 0;
lastHighestFrequencyMonth = highFreqMonths.LastOrDefault().Key;
lastLowestFrequencyMonth = lowFreqMonths.LastOrDefault().Key;
//4.If the high frequency comes first, sell short at the end of the month with the high frequency.
if (lastHighestFrequencyMonth < lastLowestFrequencyMonth)
{
if (Date[bar].Month == lastHighestFrequencyMonth)
if( ShortAtClose(bar, lastHighestFrequencyMonth.ToString()) != null)
//Cover the short at the end of the month with the low frequency.
LastPosition.Tag = (object)lastLowestFrequencyMonth;
}
//5. If the low frequency comes first, buy at the end of the month with the low frequency.
else
{
if (Date[bar].Month == lastLowestFrequencyMonth)
if( BuyAtClose(bar, lastLowestFrequencyMonth.ToString()) != null)
//Sell to exit at the end of the month with the high frequency.
LastPosition.Tag = (object)lastHighestFrequencyMonth;
}
}
}
}
}
}
public class MonthData
{
public MonthData() { }
public MonthData(int month, int year, double avgPrice)
{
Month = month; Year = year; AvgPrice = avgPrice;
}
private int _month;
public int Month
{
get { return _month; }
set { _month = value; }
}
private int _year;
public int Year
{
get { return _year; }
set { _year = value; }
}
private double _avgPrice;
public double AvgPrice
{
get { return _avgPrice; }
set { _avgPrice = value; }
}
}
}
Gene Geren (Eugene)
Wealth-Lab team