TRADERS’ TIPS

February 2026

Tips Article Thumbnail

For this month’s Traders’ Tips, the focus is Markos Katsanos’ article in this issue, “A Portfolio Diversification Strategy.” Here, we present the February 2026 Traders’ Tips code with possible implementations in various software.

You can right-click on any chart to open it in a new tab or window and view it at it’s originally supplied size, often much larger than the version printed in the magazine.

The Traders’ Tips section is provided to help the reader implement a selected technique from an article in this issue or another recent issue. The entries here are contributed by software developers or programmers for software that is capable of customization.


logo

Realtest: February 2026

Following is coding for use in the RealTest platform to implement Markos Katsanos’ concepts as described in his article in this issue, “A Portfolio Diversification Strategy.” Two RealTest scripts are provided here, one for the author’s asset allocation strategy and the other for the statistical exploration discussed in the article.

Notes:
	Markos Katsanos "A Portfolio Diversification Strategy", TASC February 2026.
	"Dynamic Asset Allocation Strategy" script
	
Import:
	DataSource:	Norgate
	IncludeList:	AGG, BIL, EFA, GBTC, GLD, QQQ, SHY, SPY
	StartDate:	2015-01-01 // earliest year all symbols had data
	EndDate:	Latest
	SaveAs:	tasc_aug_25.rtd
	
Settings:
	DataFile:	tasc_aug_25.rtd
	StartDate:	2023-09-01
	EndDate:	2025-09-01
	BarSize:	Weekly
	AccountSize:	100500

Parameters: 
	// system parameters
	EXITBARS:	from 26 to 39 step 13 def 26
	
	// asset allocation percentage parameters
	AGG_PS:	from 0 to 10 step 5 def 0
	EFA_PS:	from 5 to 10 step 5 def 5
	GBTC_PS:	from 5 to 10 step 5 def 10
	GLD_PS:	from 15 to 25 step 5 def 25
	QQQ_PS:	from 15 to 30 step 5 def 30
	SHY_PS:	from 5 to 10 step 5 def 5
	SPY_PS:	from 15 to 30 step 5 def 20

Library:
	TOTAL_PS:	AGG_PS + GBTC_PS + EFA_PS + GLD_PS + QQQ_PS + SHY_PS + SPY_PS
	BIL_PS:	IF(TOTAL_PS < 100, 100 - TOTAL_PS, 0)
	
OptimizeSettings:
	SkipTestIf:	TOTAL_PS + BIL_PS > 100

Strategy: dynamic_asset
	Side:	Long
	EntrySetup:	1
	ExitRule:	BarsHeld = EXITBARS
	ExitTime:	ThisClose
	Quantity:	Item("{?}_PS", ?Symbol) // looks up the percentage parameter for each symbol
	QtyType:	Percent
	QtyPrice:	FillPrice
	Commission:	0.01 * Shares


Notes:
	Markos Katsanos "A Portfolio Diversification Strategy", TASC February 2026.
	"Statistics Exploration" script
	
Import:
	DataSource:	Norgate 
	// symbols from table in Figure 3
	IncludeList:	SPY, QQQ, EFA, EEM, XLE, IYR, AGG, SHY, TLT, BIL, SHV, FXE, 
		UUP, DBA, DBC, FTGC, GDX, GLD, GSG, USO, BTCUSD, GBTC
	StartDate:	2005-01-01
	EndDate:	Latest
	SaveAs:	tasc_aug_25_a.rtd
	
Settings:
	DataFile:	tasc_aug_25_a.rtd
	StartDate:	2005-09-30
	EndDate:	2025-09-30
	BarSize:	Weekly
	TestOutput:	Scan
	ScanNoDefCols:	True

Data:
	barcount:	SinceTrue(BarNum = 1 or BarDate <= ToDate(?StartDate))
	maxPrice:	Highest(C, barcount)
	drawdown:	c / maxPrice - 1
	ddmax:	Lowest(drawdown, barcount)
	
Scan:
	Filter:	BarsLeft = 0 or BarDate[-1] > ToDate(?EndDate)
	ETF:	?Symbol
	Name:	?Name
	Start:	BarDate[barcount] {//}
	End:	BarDate {//}
	StartPrice:	Close[barcount]
	EndPrice:	Close
	Yrs:	barcount / 52 {#1}
	CAR:	(EndPrice/StartPrice)^(52/barcount)-1 {%2}
	WorstDD:	ddmax {%2}
	ROC3MO:	C/C[13]-1 {%2}
	RSI5:	RSI(5) {#2}
	Pain:	Abs(Sum(100 * drawdown, barcount) / Sum(drawdown < 0, barcount)) {#2}
	Ulcer:	Sqr(SumSq(100 * drawdown, barcount) / barcount) {#2}
	DDDate:	whentrue(drawdown=ddmax,BarDate) {//}
	StDev:	StdDev(ROC(C,1), barcount)
	CorSpy:	Correl(ROC(C,1), Extern($SPY, ROC(C,1)), barcount)

—Marsten Parker
MHP Trading
mhp@mhptrading.com

BACK TO LIST

logo

Wealth-Lab.com: February 2026

Markos Katsanos’ article in this issue, “A Portfolio Diversification Strategy,” discusses dynamic portfolio allocation based on walk-forward optimization of the rebalancing of the portfolio assets.

This kind of portfolio manipulation is the bread and butter of WealthLab. We approached it by creating two WealthLab strategies. The first—the baseline strategy—simply buys a portion of each symbol in the portfolio, with the amount purchased based on an optimizable parameter. The WealthLab Rebalance method makes this kind of portfolio rebalancing a breeze.

The second strategy uses WealthLab’s StrategyRunner class, which allows you to run and even optimize other strategies within WealthLab strategy code. Here, we use the PerformOptimization method of StrategyRunner to optimize the allocation values of the baseline strategy every six months. We use our built-in Shrinking Window optimizer as a “solver,” so no need to drop down to Excel. It can all just be done in WealthLab.

The resulting strategy takes a bit of time to run, since it must perform a sequence of optimizations, but the selected optimizer runs in parallel and will use all available CPU cores. WealthLab’s architecture is extensible, so other optimizers, even those developed by third parties, can be plugged in by replacing the “Shrinking Window” in the method call with the name of the desired optimizer. The solution provides a full dynamic allocation model all within the WealthLab strategy, with minimal required code.

Code for Part 1 (Baseline Strategy)
Note: Save this Strategy with a name of 2026-02-Part1, as it is referred to by name in Part 2
using WealthLab.Backtest;
using System;
using WealthLab.Core;
using WealthLab.Data;
using WealthLab.Indicators;
using System.Collections.Generic;

namespace WealthScript2 
{
    public class MyStrategy : UserStrategyBase
    {
		//create parameter instances
		public MyStrategy() : base()
		{
			paramAGG = AddParameter("AGG", ParameterType.Double, 0, 0, 10, 1);
			paramEFA = AddParameter("EFA", ParameterType.Double, 5, 5, 10, 1);
			paramGBTC = AddParameter("GBTC", ParameterType.Double, 10, 5, 10, 1);
			paramGLD = AddParameter("GLD", ParameterType.Double, 25, 15, 25, 1);
			paramQQQ = AddParameter("QQQ", ParameterType.Double, 30, 15, 30, 1);
			paramSHY = AddParameter("SHY", ParameterType.Double, 5, 5, 10, 1);
			paramSPY = AddParameter("SPY", ParameterType.Double, 20, 15, 30, 1);
		}
		
        //create indicators and other objects here, this is executed prior to the main trading loop
        public override void Initialize(BarHistory bars)
        {
        }

        //execute the strategy rules here, this is executed once for each bar in the backtest history
        public override void Execute(BarHistory bars, int idx)
        {
            if (idx == 0)
			{
				foreach(Parameter p in Parameters)
				{
					if (p.Name == bars.Symbol)
					{
						Rebalance(bars, p.AsDouble);
					}
				}
			}
        }

	//declare private variables below
	private Parameter paramAGG;
	private Parameter paramEFA;
	private Parameter paramGBTC;
	private Parameter paramGLD;
	private Parameter paramQQQ;
	private Parameter paramSHY;
	private Parameter paramSPY;
    }
}

Code for Part 2

using WealthLab.Backtest;
using System;
using WealthLab.Core;
using WealthLab.Data;
using WealthLab.Indicators;
using System.Collections.Generic;
using System.Linq;

namespace WealthScript1 
{
    public class MyStrategy : UserStrategyBase
    {
        //create indicators and other objects here, this is executed prior to the main trading loop
        public override void Initialize(BarHistory bars)
        {
        }

		//this execute prior to each individual component's Execute method, we perform ranking of participants here
		public override void PreExecute(DateTime dt, List<BarHistory> participants)
		{
			//rebalance semiannually
			if (dt.Month == 1 || dt.Month == 7)
			{
				//optimize for best annualized return for previous 3 years
				runDate = dt;
				StrategyRunner sr = new StrategyRunner();
				List<string> symbols = participants.Select(bh => bh.Symbol).ToList();
				WriteToStatusBar("Collecting history for optimization..."); 
				sr.Data = WLHost.Instance.GetHistories(symbols, HistoryScale.Monthly, dt.AddYears(-3), dt, 0, null);
				WriteToStatusBar("Performing optimization...");
				OptimizationResultList orl = sr.PerformOptimization("Shrinking Window", "2026-02-Part1", runComplete);
				OptimizationResult or = orl.FindBest("APR", true);
				WriteToStatusBar("Rebalancing");
				string s = dt.ToString("MMMyy");
				foreach(BarHistory bh in participants)
				{
					int idx = masterSymbols.IndexOf(bh.Symbol);
					double rebal = or.ParameterValues[idx];
					s += " " + bh.Symbol + "=" + rebal.ToString("N2"); 
					Rebalance(bh, rebal);
				}
				WriteToDebugLog(s, false);
			}
        }

        //execute the strategy rules here, this is executed once for each bar in the backtest history
        public override void Execute(BarHistory bars, int idx)
        {
        }

		//private members
		private static List<string> masterSymbols = new List<string>() { "AGG", "EFA", "GBTC", "GLD", "QQQ", "SHY", "SPY" };
		private static DateTime runDate;
		
		//delegate
		private void runComplete(OptimizationResult or, double pctComplete)
		{
			WriteToStatusBar(runDate.ToString("MMMyy") + " " + pctComplete.ToString("N2") + "%");
		}
	}
}

Strategy Part 2 outputs the dynamic allocations to the WealthLab Debug Window for your review:

---Sequential Debug Log---
Jan16 GLD=19.00 GBTC=10.00 EFA=7.00 QQQ=30.00 SPY=30.00 AGG=7.00 SHY=9.00
Jul16 GLD=20.00 GBTC=10.00 EFA=6.00 QQQ=25.00 SPY=30.00 AGG=1.00 SHY=8.00
Jan17 GLD=18.00 GBTC=10.00 EFA=9.00 QQQ=17.00 SPY=28.00 AGG=10.00 SHY=7.00
Jul17 GLD=23.00 GBTC=10.00 EFA=5.00 QQQ=30.00 SPY=30.00 AGG=7.00 SHY=10.00
Jan18 GLD=16.00 GBTC=10.00 EFA=6.00 QQQ=21.00 SPY=27.00 AGG=9.00 SHY=9.00
Jul18 GLD=19.00 GBTC=10.00 EFA=10.00 QQQ=21.00 SPY=26.00 AGG=1.00 SHY=7.00
Jan19 GLD=25.00 GBTC=10.00 EFA=6.00 QQQ=22.00 SPY=25.00 AGG=8.00 SHY=7.00
Jul19 GLD=17.00 GBTC=10.00 EFA=9.00 QQQ=30.00 SPY=23.00 AGG=0.00 SHY=9.00
Jan20 GLD=16.00 GBTC=9.00 EFA=9.00 QQQ=29.00 SPY=23.00 AGG=3.00 SHY=10.00
Jul20 GLD=21.00 GBTC=9.00 EFA=10.00 QQQ=30.00 SPY=20.00 AGG=4.00 SHY=6.00
Jan21 GLD=22.00 GBTC=10.00 EFA=7.00 QQQ=30.00 SPY=23.00 AGG=0.00 SHY=9.00
Jul21 GLD=17.00 GBTC=8.00 EFA=6.00 QQQ=29.00 SPY=27.00 AGG=3.00 SHY=8.00
Jan22 GLD=25.00 GBTC=9.00 EFA=8.00 QQQ=26.00 SPY=25.00 AGG=8.00 SHY=10.00
Jul22 GLD=23.00 GBTC=7.00 EFA=10.00 QQQ=30.00 SPY=28.00 AGG=2.00 SHY=7.00
Jan23 GLD=21.00 GBTC=6.00 EFA=7.00 QQQ=28.00 SPY=28.00 AGG=1.00 SHY=7.00
Jul23 GLD=25.00 GBTC=8.00 EFA=7.00 QQQ=29.00 SPY=27.00 AGG=3.00 SHY=6.00
Jan24 GLD=24.00 GBTC=7.00 EFA=8.00 QQQ=26.00 SPY=28.00 AGG=4.00 SHY=9.00
Jul24 GLD=25.00 GBTC=9.00 EFA=7.00 QQQ=25.00 SPY=23.00 AGG=0.00 SHY=6.00
Jan25 GLD=23.00 GBTC=9.00 EFA=8.00 QQQ=27.00 SPY=27.00 AGG=8.00 SHY=8.00
Jul25 GLD=22.00 GBTC=9.00 EFA=6.00 QQQ=25.00 SPY=23.00 AGG=0.00 SHY=8.00

An example equity curve for the strategy is shown in Figure 1.

Sample Chart

FIGURE 1: WEALTH-LAB. An example of an equity curve produced from a test of the dynamic asset allocation model is shown.

—Dion Kurczek
Wealth-Lab team
www.wealth-lab.com

BACK TO LIST

logo

TradingView: February 2026

The TradingView Pine Script code presented here creates a basic foundation for diversified portfolio analysis and weighting from concepts presented by Markos Katsanos in his article in this issue, “A Portfolio Diversification Strategy.”

//  TASC Issue: February 2026
//     Article: Foundational Portfolio Design, Not Stock-Picking
//              A Protfolio Diversification Strategy
//  Article By: Markos Katsanos
//    Language: TradingView's Pine Script® v6
// Provided By: PineCoders, for tradingview.com

//@version=6
indicator("TASC 2026.02 Portfolio Diversification", "TASC")

//#region Import Library

import TradingView/ta/12 as TVta

//#endregion

//#region Inputs

string TT_DD = "Maximum Drawdown"
string TT_SD = "Standard deviation"
string TT_SR = "Sharpe ratio"
string TT_PI = "Pain index"
string TT_UI = "Ulcer index"
string TT_CR = "Correlation"
string TT_TYPE = "Pre-configured portfolios as specified in the article."

// @enum Portfolio type options.
enum PRT_TYPE
    C = "Custom"
    LRISK = "Low Risk"
    CONS = "Conservative"
    AGGR = "Aggressive"
    DYNM = "Dynamic"
    
GROUP1 = "Portfolio"
GROUP2 = "Display"

string benchSym = input.symbol("SPY", "Benchmark Symbol", group = GROUP1)

PRT_TYPE prtSelected = input.enum(PRT_TYPE.C, "Portfolio Type:", 
  group = GROUP1, tooltip = TT_TYPE)
custom = prtSelected == PRT_TYPE.C

bool   use00 = input.bool(  true, "", inline="00", active = custom,
  group = GROUP1)
string sym00 = input.symbol("AGG", "", inline="00", active = custom,
  group = GROUP1)
int    qty00 = input.int(       1, "", inline="00", active = custom,
  group = GROUP1)
bool   use01 = input.bool(  true, "", inline="01", active = custom,
  group = GROUP1)
string sym01 = input.symbol("BIL", "", inline="01", active = custom,
  group = GROUP1)
int    qty01 = input.int(       1, "", inline="01", active = custom,
  group = GROUP1)
bool   use02 = input.bool(  true, "", inline="02", active = custom,
  group = GROUP1)
string sym02 = input.symbol("EFA", "", inline="02", active = custom,
  group = GROUP1)
int    qty02 = input.int(       1, "", inline="02", active = custom,
  group = GROUP1)
bool   use03 = input.bool(  true, "", inline="03", active = custom,
  group = GROUP1)
string sym03 = input.symbol("GBTC", "", inline="03", active = custom,
  group = GROUP1)
int    qty03 = input.int(       1, "", inline="03", active = custom,
  group = GROUP1)
bool   use04 = input.bool(  true, "", inline="04", active = custom,
  group = GROUP1)
string sym04 = input.symbol("GLD", "", inline="04", active = custom,
  group = GROUP1)
int    qty04 = input.int(       1, "", inline="04", active = custom,
  group = GROUP1)
bool   use05 = input.bool(  true, "", inline="05", active = custom,
  group = GROUP1)
string sym05 = input.symbol("QQQ", "", inline="05", active = custom,
  group = GROUP1)
int    qty05 = input.int(       1, "", inline="05", active = custom,
  group = GROUP1)
bool   use06 = input.bool(  true, "", inline="06", active = custom,
  group = GROUP1)
string sym06 = input.symbol("SHY", "", inline="06", active = custom,
  group = GROUP1)
int    qty06 = input.int(       1, "", inline="06", active = custom,
  group = GROUP1)
bool   use07 = input.bool(  true, "", inline="07", active = custom,
  group = GROUP1)
string sym07 = input.symbol("SPY", "", inline="07", active = custom,
  group = GROUP1)
int    qty07 = input.int(       1, "", inline="07", active = custom,
  group = GROUP1)
bool   use08 = input.bool(  true, "", inline="08", active = custom,
  group = GROUP1)
string sym08 = input.symbol("DBA", "", inline="08", active = custom,
  group = GROUP1)
int    qty08 = input.int(       1, "", inline="08", active = custom,
  group = GROUP1)
bool   use09 = input.bool(  true, "", inline="09", active = custom,
  group = GROUP1)
string sym09 = input.symbol("USO", "", inline="09", active = custom,
  group = GROUP1)
int    qty09 = input.int(       1, "", inline="09", active = custom,
  group = GROUP1)

bool show_returns_table = input.bool(true, "Show Return Table",
  group = GROUP2)
bool show_statistics_table = input.bool(true, "Show Stat Table",
  group = GROUP2)

//#endregion

//#region Symbol & Pre-configured Portfolio Setup

// @function Create Custom symbol.
CustomSymbol () =>
    string _sym = "0"
    if use00
        _sym += str.format("+{0}*{1}", sym00, qty00)
    if use01
        _sym += str.format("+{0}*{1}", sym01, qty01)
    if use02
        _sym += str.format("+{0}*{1}", sym02, qty02)
    if use03
        _sym += str.format("+{0}*{1}", sym03, qty03)
    if use04
        _sym += str.format("+{0}*{1}", sym04, qty04)
    if use05
        _sym += str.format("+{0}*{1}", sym05, qty05)
    if use06
        _sym += str.format("+{0}*{1}", sym06, qty06)
    if use07
        _sym += str.format("+{0}*{1}", sym07, qty07)
    if use08
        _sym += str.format("+{0}*{1}", sym08, qty08)
    if use09
        _sym += str.format("+{0}*{1}", sym09, qty09)
    if _sym == "0"
        _sym := ""
    request.security(_sym, "", close)

string S00 = ("AGG*02+BIL*30+EFA*07+GLD*17+QQQ*13+SHY*15"+
            "+SPY*16")
string S01 = ("AGG*10+BIL*05+EFA*10+GBTC*03+GLD*17+QQQ*18"+
            "+SHY*15+SPY*22")
string S02 = ("AGG*03+EFA*03+GBTC*07+GLD*17+QQQ*38+SHY*12"+
            "+SPY*20")
string S03 = ("BIL*05+EFA*05+GBTC*10+GLD*25+QQQ*30+SHY*05"+
            "+SPY*20")

sec(_sym) => request.security(_sym, "", close)

float sym = switch prtSelected
    PRT_TYPE.LRISK => sec(S00)
    PRT_TYPE.CONS  => sec(S01)
    PRT_TYPE.AGGR  => sec(S02)
    PRT_TYPE.DYNM  => sec(S03)
    PRT_TYPE.C     => CustomSymbol()
    => float(na)
float bench = sec(benchSym)

//#endregion

//#region Drawdown

var float hh = nz(sym)
var float ddm = 0.0
var int ddd = 0
var float ddc = 0.0
float dd = (sym / hh) - 1.0
if sym >= hh
    hh := sym
if dd <= ddm
    ddm := dd
if dd < 0
    ddd += 1
    ddc += dd

//#endregion

//#region Portfolio & Benchmark Returns

bool is_new_year = year != year[1]

var float prt_y_open = sym
var float spy_y_open = bench
if is_new_year
    prt_y_open := sym
    spy_y_open := bench
float prt_y_perc = sym / prt_y_open - 1.0
float spy_y_perc = bench / spy_y_open - 1.0
var int[] years = array.new<int>(0)
var float[] prt_y_returns = array.new<float>(0)
var float[] spy_y_returns = array.new<float>(0)
if is_new_year and not na(prt_y_perc[1])
    prt_y_returns.unshift(prt_y_perc[1])
    spy_y_returns.unshift(spy_y_perc[1])
    years.unshift(year[1])

//#endregion

//#region Stat Calculations

//Sharpe
var er_ary = array.new_float()
sym_r = (sym / sym[1]) - 1 
//Risk-Free Return Rate
rf = request.security("TVC:US10Y", timeframe.period, close/25200)
if not na(rf)
    er_ary.push(sym_r - rf)
er_avg = er_ary.avg()
std =  er_ary.stdev()

float sharpe =  er_avg / std * math.sqrt(252)

//Others
float pain = ddc / (ddd + 0.000001)
float ulcer = TVta.ulcerIndex(sym, 14)
float corr = ta.correlation(sym , bench, 14)

//#endregion


//#region Display

//Plots
hline(0)
plot(prt_y_perc, "Annualized Portfolio P&L %", color.blue, 
  style = plot.style_linebr)
plot(na(prt_y_perc)?na:ddm, "Max Drawdown %", color.silver, 
  style = plot.style_linebr)
plot(na(prt_y_perc)?na:spy_y_perc, "Annualized SPY P&L %", color.red, 
  style = plot.style_linebr)

//Table Colors
C0 = color.rgb(0,0,0,100)
C1 = chart.fg_color
P0 = position.bottom_right
P1 = position.bottom_left

//Method for Cell formatting
method clabel (table T, int x, int y, string txt, 
        color C=chart.fg_color, string tt="") =>
      T.cell(x, y, txt, 0,0, C1, tooltip = tt, 
        text_formatting=text.format_bold)


//Table
if show_statistics_table
    var table T = table.new(P0, 2, 8, C0, C1, 2, C1, 1)
    T.clabel(0, 1, "Value")
    T.clabel(0, 2, "Drawdown",tt=TT_DD)
    T.clabel(0, 3, "STD", tt=TT_SD)
    T.clabel(0, 4, "Sharpe", tt=TT_SR)
    T.clabel(0, 5, "Pain", tt=TT_PI)
    T.clabel(0, 6, "Ulcer", tt=TT_UI)
    T.clabel(0, 7, "Correlation", tt=TT_CR)
    T.clabel(1, 1, str.format("{0}", sym))

    T.cell(1, 2,str.format("{0,number,percent}",ddm),0,0,C1)
    T.cell(1, 3, str.format("{0}", std), text_color=C1)
    T.cell(1, 4, str.format("{0}", sharpe), text_color=C1)
    T.cell(1, 5, str.format("{0}", pain), text_color=C1)
    T.cell(1, 6, str.format("{0}", ulcer), text_color=C1)
    T.cell(1, 7, str.format("{0}", corr), text_color=C1)

y_sz = prt_y_returns.size()
if show_returns_table and y_sz > 0
    var table T_returns = table.new(P1,22,3,C0, C1,2, C1,1)
    T_returns.clabel(0, 0, "Returns /Y")
    T_returns.clabel(0, 1, "Portfolio")
    T_returns.clabel(0, 2, "SPY")
    index = math.min(20, prt_y_returns.size()-1)
    if is_new_year
        for _i = 0 to index
            T_returns.clabel(index+1 - _i, 0, 
                str.format("{0, number, 0000}", 
                    years.get(_i)))
            T_returns.cell(index+1 - _i, 1, 
                str.format("{0, number, percent}", 
                    prt_y_returns.get(_i)), 
                text_color=C1)
            T_returns.cell(index+1 - _i, 2, 
                str.format("{0, number, percent}", 
                    spy_y_returns.get(_i)), 
                text_color=C1)

//#endregion

Example output is shown in Figure 2.

Sample Chart

FIGURE 2: TRADINGVIEW. In this example chart output, the bottom pane displays sample annualized returns from the low-risk profile of the allocation strategy along with annualized SPY returns for comparison. The top pane is a daily chart of ES. To the right is a display of risk metrics, and at the bottom are return values (profit & loss) in tabular format.

The indicator is available on TradingView from the PineCodersTASC account at: https://www.tradingview.com/u/PineCodersTASC/#published-scripts.

—PineCoders, for TradingView
www.TradingView.com

BACK TO LIST

Python: February 2026

To help Python users implement some of the concepts described in Markos Katsanos’ article in this issue, “A Portfolio Diversification Strategy,” Python code is provided below.

"""

# Import required python libraries

%matplotlib inline

import pandas as pd
import numpy as np
import yfinance as yf

import math
import datetime as dt
import matplotlib.pyplot as plt
import mplfinance as mpf

print(yf.__version__)


# The following are key building blocks and helper functions 
# to test and implement associated article concepts

def get_allocation_schemes():
    
    data = {
        'Ticker': ['AGG', 'BIL', 'EFA', 'GBTC', 'GLD', 'QQQ', 'SHY', 'SPY'],
        'Asset Class': ['Bonds', 'Cash', 'Equities', 'Alternative',
                        'Commmodities', 'Equities', 'Bonds', 'Equities'],
        'Adjusted':     [15, 0, 10, 0, 15, 20, 15, 20],
        'Low Risk':     [2, 30, 7, 0, 17, 13, 15, 16],
        'Conservative': [10, 5, 10, 3, 17, 18, 15, 22],
        'Aggressive':   [3, 0, 3, 7, 17, 38, 12, 20],
        'Dynamic':      [0, 5, 5, 10, 25, 30, 5, 20]
    }

    df = pd.DataFrame(data).set_index('Ticker')

    # ensure numeric allocations
    alloc_cols = df.columns.difference(['Asset Class'])
    df[alloc_cols] = df[alloc_cols].astype(float)

    return df

    
def get_scheme_weights(
    alloc_df: pd.DataFrame,
    scheme: str
) -> dict[str, float]:
    """
    Return normalized ticker weights for a given allocation scheme.

    Parameters
    ----------
    alloc_df : pd.DataFrame
        Allocation table indexed by Ticker.
    scheme : str
        Allocation scheme name (e.g. 'Aggressive').

    Returns
    -------
    dict
        {ticker: weight} where weights sum to 1.0
    """

    if scheme not in alloc_df.columns:
        raise ValueError(f"Scheme '{scheme}' not found in allocation table.")

    # Extract raw weights
    w = alloc_df[scheme].astype(float)

    # Replace NaN with zero (explicitly no allocation)
    w = w.fillna(0.0)

    total = w.sum()
    if total <= 0:
        raise ValueError(f"Scheme '{scheme}' has zero total weight.")

    # Normalize
    w = w / total

    return w.to_dict()


# Use Yahoo Finance python package to obtain OHLCV data for desired tickers

def get_yahoo_ohlcv(
    tickers: list[str],
    start: str,
    end: str
) -> pd.DataFrame:
    """
    Download historical OHLCV price data from Yahoo Finance.

    Parameters
    ----------
    tickers : list[str] or str
        One or more ticker symbols accepted by Yahoo Finance.
        Examples: ['SPY','AGG','GLD'] or 'SPY'
    start : str or pd.Timestamp
        Start date for historical data (inclusive).
        Format: 'YYYY-MM-DD' or pandas Timestamp.
    end : str or pd.Timestamp
        End date for historical data (inclusive).
        Format: 'YYYY-MM-DD' or pandas Timestamp.

    Returns
    -------
    pd.DataFrame
        OHLCV price data as returned by yfinance.
        - Index: DatetimeIndex
        - Columns: MultiIndex (Price field × Ticker)
        - Price fields typically include:
          ['Open','High','Low','Close','Volume']
        - Prices are adjusted if auto_adjust=True
    """

    ohlcv = yf.download(
        tickers,
        start,
        end,
        auto_adjust=True,     # adjusts prices for splits/dividends
        progress=False        # suppress download progress output
    )

    return ohlcv

def resample_ohlcv(df, tsagg='W'):
    """
    Resample yfinance-style OHLCV DataFrame to a new time aggregation.

    Parameters
    ----------
    df : pd.DataFrame
        Daily OHLCV with MultiIndex columns (Price × Ticker)
    tsagg : str
        'W' = weekly (Fri close)
        'M' = business month-end
        'Q' = business quarter-end
        'Y' = business year-end

    Returns
    -------
    pd.DataFrame
        Resampled OHLCV with same MultiIndex structure
    """

    if not isinstance(df.columns, pd.MultiIndex):
        raise TypeError("df must have MultiIndex columns (Price × Ticker)")

    freq_map = {
        'W': 'W-FRI',
        'M': 'BM',
        'Q': 'BQ',
        'Y': 'BA'
    }
    tsagg = freq_map.get(tsagg, tsagg)

    resampled = []

    # Vectorized across tickers (FAST)
    for price, how in {
        'Open': 'first',
        'High': 'max',
        'Low': 'min',
        'Close': 'last',
        'Volume': 'sum'
    }.items():

        tmp = getattr(df[price].resample(tsagg), how)()

        tmp.columns = pd.MultiIndex.from_product([[price], tmp.columns])
        resampled.append(tmp)

    return pd.concat(resampled, axis=1).sort_index(axis=1)


def get_data_coverage(close_df: pd.DataFrame) -> pd.DataFrame:
    """
    Return start and end dates of valid data for each ticker, sorted by start date.

    Parameters
    ----------
    close_df : pd.DataFrame
        Close prices (index = DatetimeIndex, columns = tickers)

    Returns
    -------
    pd.DataFrame
        Columns: ['Ticker', 'Start', 'End'], sorted by Start
    """

    if not isinstance(close_df.index, pd.DatetimeIndex):
        raise TypeError("close_df index must be a DatetimeIndex")

    records = []

    for ticker in close_df.columns:
        s = close_df[ticker].dropna()
        if s.empty:
            start, end = pd.NaT, pd.NaT
        else:
            start, end = s.index[0], s.index[-1]

        records.append({
            'Ticker': ticker,
            'Start': start,
            'End': end
        })

    coverage_df = pd.DataFrame(records)

    # Sort by start date
    coverage_df = coverage_df.sort_values('Start').reset_index(drop=True)

    return coverage_df

def get_rebalance_dates(prices: pd.DataFrame, rebalance_freq: str = 'M') -> pd.DatetimeIndex:
    """
    Determine rebalance dates based on weekly prices and desired frequency.
    Validates that all rebalance dates exist in the prices index.

    Parameters
    ----------
    prices : pd.DataFrame
        Cleaned weekly prices. Index = DatetimeIndex
    rebalance_freq : str
        'W', 'M', 'Q', 'H', 'Y'

    Returns
    -------
    pd.DatetimeIndex
        Last weekly date of each period for rebalancing.
    """
    freq_map = {'W':'W-FRI', 'M':'M', 'Q':'Q', 'H':'2Q', 'Y':'A'}
    freq = freq_map.get(rebalance_freq, rebalance_freq)

    # Group by period, take last available weekly date
    rebalance_dates = prices.groupby(pd.Grouper(freq=freq)).apply(lambda x: x.index[-1])
    rebalance_dates = pd.to_datetime(rebalance_dates.drop_duplicates().values)

    # Sanity check: ensure all dates exist in prices index
    missing_dates = rebalance_dates.difference(prices.index)
    if not missing_dates.empty:
        raise ValueError(f"The following rebalance dates are missing in prices index: {missing_dates}")

    return rebalance_dates

def open_portfolio_positions(close_df: pd.DataFrame,
                              real_date: pd.Timestamp,
                              weights_dict: dict,
                              capital_to_invest: float) -> pd.DataFrame:
    """
    Compute portfolio positions given close prices, target weights, and capital.

    Parameters
    ----------
    close_df : pd.DataFrame
        Weekly closing prices. Index: DatetimeIndex, columns: tickers.
    real_date : pd.Timestamp
        Date at which to take the prices.
    weights_dict : dict
        Target weights {ticker: weight}.
    capital_to_invest : float
        Total capital to invest.

    Returns
    -------
    pd.DataFrame
        Columns: ['symbol','qty','buy_price','cost_basis','target_weight']
    """
    # Align real_date to last available date <= real_date
    if real_date not in close_df.index:
        real_date = close_df.index[close_df.index.get_loc(real_date, method='pad')]
    
    prices = close_df.loc[real_date]

    # Keep only tickers that have valid prices and are in weights_dict
    tickers = [t for t in close_df.columns if t in weights_dict and not np.isnan(prices[t])]
    
    data = []
    for t in tickers:
        w = weights_dict[t]
        price = prices[t]
        dollar_allocation = capital_to_invest * w
        qty = int(dollar_allocation // price)  # round down to integer shares
        cost_basis = qty * price
        data.append([ t, qty, real_date,price, cost_basis, w])
    
    positions_df = pd.DataFrame(data, columns=['symbol','qty','buy_date','buy_price','cost_basis','target_weight'])
    return positions_df.round(2)

def portfolio_value_on_date(positions_df: pd.DataFrame,
                            close_df: pd.DataFrame,
                            date: pd.Timestamp) -> pd.DataFrame:
    """
    Compute portfolio value and PnL for given positions on a specific date.

    Parameters
    ----------
    positions_df : pd.DataFrame
        Positions with columns ['symbol','qty','buy_price','cost_basis','target_weight'].
    close_df : pd.DataFrame
        Weekly closing prices. Index: DatetimeIndex, columns: tickers.
    date : pd.Timestamp
        Date at which to compute portfolio value and PnL.

    Returns
    -------
    pd.DataFrame
        positions_df with additional columns: 'date', 'close', 'pnl'
    """
    # Align date to last available date <= date
    if date not in close_df.index:
        date = close_df.index[close_df.index.get_loc(date, method='pad')]

    close_prices = close_df.loc[date]

    # Compute current close and PnL
    positions_df = positions_df.copy()
    positions_df['date'] = date
    positions_df['close'] = positions_df['symbol'].map(close_prices)
    positions_df['market_value'] = positions_df['close'] * positions_df['qty']
    positions_df['unrealized_pnl'] = (positions_df['close'] - positions_df['buy_price']) * positions_df['qty']

    return positions_df.round(2)

def close_portfolio_positions(
    positions_df: pd.DataFrame,
    close_df: pd.DataFrame,
    sell_date: pd.Timestamp
) -> pd.DataFrame:
    """
    Close all portfolio positions at market close and compute realized PnL.

    Parameters
    ----------
    positions_df : pd.DataFrame
        Open positions with columns:
        ['symbol','qty','buy_price','buy_date','cost_basis','target_weight']
    close_df : pd.DataFrame
        Weekly closing prices. Index: DatetimeIndex, columns: tickers.
    sell_date : pd.Timestamp
        Desired sell date (aligned to last available close).

    Returns
    -------
    pd.DataFrame
        Closed positions with columns:
        ['symbol','qty','buy_date','buy_price','sell_date','sell_price',
         'cost_basis','proceeds','realized_pnl',
         'target_weight','actual_weight']
    """

    # ---- Align sell date ----
    if sell_date not in close_df.index:
        sell_date = close_df.index[close_df.index.get_loc(sell_date, method='pad')]

    sell_prices = close_df.loc[sell_date]

    closed = positions_df.copy()
    closed['sell_date'] = sell_date
    closed['sell_price'] = closed['symbol'].map(sell_prices)

    # ---- Proceeds and realized PnL ----
    closed['proceeds'] = closed['qty'] * closed['sell_price']
    closed['realized_pnl'] = closed['proceeds'] - closed['cost_basis']

    # ---- Actual weights at liquidation ----
    total_proceeds = closed['proceeds'].sum()
    closed['actual_weight'] = (
        closed['proceeds'] / total_proceeds if total_proceeds > 0 else 0.0
    )

    # ---- Clean column order ----
    closed = closed[
        ['symbol','qty','buy_date','buy_price',
         'sell_date','sell_price',
         'cost_basis','proceeds','realized_pnl',
         'target_weight','actual_weight']
    ]

    return closed.round(2)

def normalized_returns_between_dates(
    close_df: pd.DataFrame,
    start_date: pd.Timestamp,
    end_date: pd.Timestamp
) -> pd.DataFrame:
    """
    Normalize close prices to 1.0 at start_date for comparison.

    Parameters
    ----------
    close_df : pd.DataFrame
        Weekly close prices. Index: DatetimeIndex, columns: tickers.
    start_date : pd.Timestamp
        Start date for normalization.
    end_date : pd.Timestamp
        End date for normalization.

    Returns
    -------
    pd.DataFrame
        Normalized close prices (start = 1.0) for each ticker.
    """

    df = close_df[buy_date:sell_date].copy()
    df = (1+df.pct_change()).cumprod()
    df[:1] = 1
    
    return df

def plot_normalized_returns(
    normalized_df: pd.DataFrame,
    title: str = "Normalized Returns Plot",
    figsize: tuple = (10, 6)
):
    """
    Plot normalized returns with legend outside and columns ordered
    by final cumulative return (largest gain first).

    Parameters
    ----------
    normalized_df : pd.DataFrame
        Normalized return series (index: dates, columns: tickers).
    title : str
        Plot title.
    figsize : tuple
        Figure size.
    """

    if normalized_df.empty:
        raise ValueError("normalized_df is empty")

    # ---- Sort columns by final value (descending) ----
    final_values = normalized_df.iloc[-1]
    sorted_cols = final_values.sort_values(ascending=False).index
    plot_df = normalized_df[sorted_cols]

    # ---- Plot ----
    fig, ax = plt.subplots(figsize=figsize)
    plot_df.plot(ax=ax, linewidth=2)

    ax.set_title(title)
    ax.set_ylabel("Growth Multiplier (aka value * amount_invested")
    ax.grid(True)

    # ---- Legend outside right ----
    ax.legend(
        loc="center left",
        bbox_to_anchor=(1.02, 0.5),
        frameon=False
    )

    plt.tight_layout()
    plt.show()
def get_allocation_schemes():
    
    data = {
        'Ticker': ['AGG', 'BIL', 'EFA', 'GBTC', 'GLD', 'QQQ', 'SHY', 'SPY'],
        'Asset Class': ['Bonds', 'Cash', 'Equities', 'Alternative',
                        'Commmodities', 'Equities', 'Bonds', 'Equities'],
        'Adjusted':     [15, 0, 10, 0, 15, 20, 15, 20],
        'Low Risk':     [2, 30, 7, 0, 17, 13, 15, 16],
        'Conservative': [10, 5, 10, 3, 17, 18, 15, 22],
        'Aggressive':   [3, 0, 3, 7, 17, 38, 12, 20],
        'Dynamic':      [0, 5, 5, 10, 25, 30, 5, 20]
    }

    df = pd.DataFrame(data).set_index('Ticker')

    # ensure numeric allocations
    alloc_cols = df.columns.difference(['Asset Class'])
    df[alloc_cols] = df[alloc_cols].astype(float)

    return df

    
def get_scheme_weights(
    alloc_df: pd.DataFrame,
    scheme: str
) -> dict[str, float]:
    """
    Return normalized ticker weights for a given allocation scheme.

    Parameters
    ----------
    alloc_df : pd.DataFrame
        Allocation table indexed by Ticker.
    scheme : str
        Allocation scheme name (e.g. 'Aggressive').

    Returns
    -------
    dict
        {ticker: weight} where weights sum to 1.0
    """

    if scheme not in alloc_df.columns:
        raise ValueError(f"Scheme '{scheme}' not found in allocation table.")

    # Extract raw weights
    w = alloc_df[scheme].astype(float)

    # Replace NaN with zero (explicitly no allocation)
    w = w.fillna(0.0)

    total = w.sum()
    if total <= 0:
        raise ValueError(f"Scheme '{scheme}' has zero total weight.")

    # Normalize
    w = w / total

    return w.to_dict()


# Use Yahoo Finance python package to obtain OHLCV data for desired tickers

def get_yahoo_ohlcv(
    tickers: list[str],
    start: str,
    end: str
) -> pd.DataFrame:
    """
    Download historical OHLCV price data from Yahoo Finance.

    Parameters
    ----------
    tickers : list[str] or str
        One or more ticker symbols accepted by Yahoo Finance.
        Examples: ['SPY','AGG','GLD'] or 'SPY'
    start : str or pd.Timestamp
        Start date for historical data (inclusive).
        Format: 'YYYY-MM-DD' or pandas Timestamp.
    end : str or pd.Timestamp
        End date for historical data (inclusive).
        Format: 'YYYY-MM-DD' or pandas Timestamp.

    Returns
    -------
    pd.DataFrame
        OHLCV price data as returned by yfinance.
        - Index: DatetimeIndex
        - Columns: MultiIndex (Price field × Ticker)
        - Price fields typically include:
          ['Open','High','Low','Close','Volume']
        - Prices are adjusted if auto_adjust=True
    """

    ohlcv = yf.download(
        tickers,
        start,
        end,
        auto_adjust=True,     # adjusts prices for splits/dividends
        progress=False        # suppress download progress output
    )

    return ohlcv

def resample_ohlcv(df, tsagg='W'):
    """
    Resample yfinance-style OHLCV DataFrame to a new time aggregation.

    Parameters
    ----------
    df : pd.DataFrame
        Daily OHLCV with MultiIndex columns (Price × Ticker)
    tsagg : str
        'W' = weekly (Fri close)
        'M' = business month-end
        'Q' = business quarter-end
        'Y' = business year-end

    Returns
    -------
    pd.DataFrame
        Resampled OHLCV with same MultiIndex structure
    """

    if not isinstance(df.columns, pd.MultiIndex):
        raise TypeError("df must have MultiIndex columns (Price × Ticker)")

    freq_map = {
        'W': 'W-FRI',
        'M': 'BM',
        'Q': 'BQ',
        'Y': 'BA'
    }
    tsagg = freq_map.get(tsagg, tsagg)

    resampled = []

    # Vectorized across tickers (FAST)
    for price, how in {
        'Open': 'first',
        'High': 'max',
        'Low': 'min',
        'Close': 'last',
        'Volume': 'sum'
    }.items():

        tmp = getattr(df[price].resample(tsagg), how)()

        tmp.columns = pd.MultiIndex.from_product([[price], tmp.columns])
        resampled.append(tmp)

    return pd.concat(resampled, axis=1).sort_index(axis=1)


def get_data_coverage(close_df: pd.DataFrame) -> pd.DataFrame:
    """
    Return start and end dates of valid data for each ticker, sorted by start date.

    Parameters
    ----------
    close_df : pd.DataFrame
        Close prices (index = DatetimeIndex, columns = tickers)

    Returns
    -------
    pd.DataFrame
        Columns: ['Ticker', 'Start', 'End'], sorted by Start
    """

    if not isinstance(close_df.index, pd.DatetimeIndex):
        raise TypeError("close_df index must be a DatetimeIndex")

    records = []

    for ticker in close_df.columns:
        s = close_df[ticker].dropna()
        if s.empty:
            start, end = pd.NaT, pd.NaT
        else:
            start, end = s.index[0], s.index[-1]

        records.append({
            'Ticker': ticker,
            'Start': start,
            'End': end
        })

    coverage_df = pd.DataFrame(records)

    # Sort by start date
    coverage_df = coverage_df.sort_values('Start').reset_index(drop=True)

    return coverage_df

def get_rebalance_dates(prices: pd.DataFrame, rebalance_freq: str = 'M') -> pd.DatetimeIndex:
    """
    Determine rebalance dates based on weekly prices and desired frequency.
    Validates that all rebalance dates exist in the prices index.

    Parameters
    ----------
    prices : pd.DataFrame
        Cleaned weekly prices. Index = DatetimeIndex
    rebalance_freq : str
        'W', 'M', 'Q', 'H', 'Y'

    Returns
    -------
    pd.DatetimeIndex
        Last weekly date of each period for rebalancing.
    """
    freq_map = {'W':'W-FRI', 'M':'M', 'Q':'Q', 'H':'2Q', 'Y':'A'}
    freq = freq_map.get(rebalance_freq, rebalance_freq)

    # Group by period, take last available weekly date
    rebalance_dates = prices.groupby(pd.Grouper(freq=freq)).apply(lambda x: x.index[-1])
    rebalance_dates = pd.to_datetime(rebalance_dates.drop_duplicates().values)

    # Sanity check: ensure all dates exist in prices index
    missing_dates = rebalance_dates.difference(prices.index)
    if not missing_dates.empty:
        raise ValueError(f"The following rebalance dates are missing in prices index: {missing_dates}")

    return rebalance_dates

def open_portfolio_positions(close_df: pd.DataFrame,
                              real_date: pd.Timestamp,
                              weights_dict: dict,
                              capital_to_invest: float) -> pd.DataFrame:
    """
    Compute portfolio positions given close prices, target weights, and capital.

    Parameters
    ----------
    close_df : pd.DataFrame
        Weekly closing prices. Index: DatetimeIndex, columns: tickers.
    real_date : pd.Timestamp
        Date at which to take the prices.
    weights_dict : dict
        Target weights {ticker: weight}.
    capital_to_invest : float
        Total capital to invest.

    Returns
    -------
    pd.DataFrame
        Columns: ['symbol','qty','buy_price','cost_basis','target_weight']
    """
    # Align real_date to last available date <= real_date
    if real_date not in close_df.index:
        real_date = close_df.index[close_df.index.get_loc(real_date, method='pad')]
    
    prices = close_df.loc[real_date]

    # Keep only tickers that have valid prices and are in weights_dict
    tickers = [t for t in close_df.columns if t in weights_dict and not np.isnan(prices[t])]
    
    data = []
    for t in tickers:
        w = weights_dict[t]
        price = prices[t]
        dollar_allocation = capital_to_invest * w
        qty = int(dollar_allocation // price)  # round down to integer shares
        cost_basis = qty * price
        data.append([ t, qty, real_date,price, cost_basis, w])
    
    positions_df = pd.DataFrame(data, columns=['symbol','qty','buy_date','buy_price','cost_basis','target_weight'])
    return positions_df.round(2)

def portfolio_value_on_date(positions_df: pd.DataFrame,
                            close_df: pd.DataFrame,
                            date: pd.Timestamp) -> pd.DataFrame:
    """
    Compute portfolio value and PnL for given positions on a specific date.

    Parameters
    ----------
    positions_df : pd.DataFrame
        Positions with columns ['symbol','qty','buy_price','cost_basis','target_weight'].
    close_df : pd.DataFrame
        Weekly closing prices. Index: DatetimeIndex, columns: tickers.
    date : pd.Timestamp
        Date at which to compute portfolio value and PnL.

    Returns
    -------
    pd.DataFrame
        positions_df with additional columns: 'date', 'close', 'pnl'
    """
    # Align date to last available date <= date
    if date not in close_df.index:
        date = close_df.index[close_df.index.get_loc(date, method='pad')]

    close_prices = close_df.loc[date]

    # Compute current close and PnL
    positions_df = positions_df.copy()
    positions_df['date'] = date
    positions_df['close'] = positions_df['symbol'].map(close_prices)
    positions_df['market_value'] = positions_df['close'] * positions_df['qty']
    positions_df['unrealized_pnl'] = (positions_df['close'] - positions_df['buy_price']) * positions_df['qty']

    return positions_df.round(2)

def close_portfolio_positions(
    positions_df: pd.DataFrame,
    close_df: pd.DataFrame,
    sell_date: pd.Timestamp
) -> pd.DataFrame:
    """
    Close all portfolio positions at market close and compute realized PnL.

    Parameters
    ----------
    positions_df : pd.DataFrame
        Open positions with columns:
        ['symbol','qty','buy_price','buy_date','cost_basis','target_weight']
    close_df : pd.DataFrame
        Weekly closing prices. Index: DatetimeIndex, columns: tickers.
    sell_date : pd.Timestamp
        Desired sell date (aligned to last available close).

    Returns
    -------
    pd.DataFrame
        Closed positions with columns:
        ['symbol','qty','buy_date','buy_price','sell_date','sell_price',
         'cost_basis','proceeds','realized_pnl',
         'target_weight','actual_weight']
    """

    # ---- Align sell date ----
    if sell_date not in close_df.index:
        sell_date = close_df.index[close_df.index.get_loc(sell_date, method='pad')]

    sell_prices = close_df.loc[sell_date]

    closed = positions_df.copy()
    closed['sell_date'] = sell_date
    closed['sell_price'] = closed['symbol'].map(sell_prices)

    # ---- Proceeds and realized PnL ----
    closed['proceeds'] = closed['qty'] * closed['sell_price']
    closed['realized_pnl'] = closed['proceeds'] - closed['cost_basis']

    # ---- Actual weights at liquidation ----
    total_proceeds = closed['proceeds'].sum()
    closed['actual_weight'] = (
        closed['proceeds'] / total_proceeds if total_proceeds > 0 else 0.0
    )

    # ---- Clean column order ----
    closed = closed[
        ['symbol','qty','buy_date','buy_price',
         'sell_date','sell_price',
         'cost_basis','proceeds','realized_pnl',
         'target_weight','actual_weight']
    ]

    return closed.round(2)

def normalized_returns_between_dates(
    close_df: pd.DataFrame,
    start_date: pd.Timestamp,
    end_date: pd.Timestamp
) -> pd.DataFrame:
    """
    Normalize close prices to 1.0 at start_date for comparison.

    Parameters
    ----------
    close_df : pd.DataFrame
        Weekly close prices. Index: DatetimeIndex, columns: tickers.
    start_date : pd.Timestamp
        Start date for normalization.
    end_date : pd.Timestamp
        End date for normalization.

    Returns
    -------
    pd.DataFrame
        Normalized close prices (start = 1.0) for each ticker.
    """

    df = close_df[buy_date:sell_date].copy()
    df = (1+df.pct_change()).cumprod()
    df[:1] = 1
    
    return df

def plot_normalized_returns(
    normalized_df: pd.DataFrame,
    title: str = "Normalized Returns Plot",
    figsize: tuple = (10, 6)
):
    """
    Plot normalized returns with legend outside and columns ordered
    by final cumulative return (largest gain first).

    Parameters
    ----------
    normalized_df : pd.DataFrame
        Normalized return series (index: dates, columns: tickers).
    title : str
        Plot title.
    figsize : tuple
        Figure size.
    """

    if normalized_df.empty:
        raise ValueError("normalized_df is empty")

    # ---- Sort columns by final value (descending) ----
    final_values = normalized_df.iloc[-1]
    sorted_cols = final_values.sort_values(ascending=False).index
    plot_df = normalized_df[sorted_cols]

    # ---- Plot ----
    fig, ax = plt.subplots(figsize=figsize)
    plot_df.plot(ax=ax, linewidth=2)

    ax.set_title(title)
    ax.set_ylabel("Growth Multiplier (aka value * amount_invested")
    ax.grid(True)

    # ---- Legend outside right ----
    ax.legend(
        loc="center left",
        bbox_to_anchor=(1.02, 0.5),
        frameon=False
    )

    plt.tight_layout()
    plt.show()

 


# Example usages


# Get allocation schemes
alloc_df = get_allocation_schemes()
alloc_df

# get weights for a specific scheme
scheme = 'Aggressive'
weights_dict = get_scheme_weights(alloc_df, scheme)  
weights_dict

{'AGG': 0.03,
 'BIL': 0.0,
 'EFA': 0.03,
 'GBTC': 0.07,
 'GLD': 0.17,
 'QQQ': 0.38,
 'SHY': 0.12,
 'SPY': 0.2}


# Use Yahoo Finance python package to obtain OHLCV data for desired tickers
tickers = alloc_df.index.tolist()
start = "1990-01-01"
end = '2025-12-18'
ohlcv = get_yahoo_ohlcv(tickers, start, end)

# resample to weekly ohlcv
weekly_ohlcv = resample_ohlcv(ohlcv, tsagg='W')
weekly_ohlcv

# extract weekly close values
close_df = weekly_ohlcv['Close'].copy()


# show coverage of price data
get_data_coverage(close_df)

# set rebalance_freq to monthly, quarterly, half-year and full year
rebalance_dates = get_rebalance_dates(close_df['2021':], rebalance_freq = 'Y')
rebalance_dates

DatetimeIndex(['2021-12-31', '2022-12-30', '2023-12-29', '2024-12-27',
               '2025-12-19'],
              dtype='datetime64[ns]', freq=None)


# open a portfolio based on capital to invest and desired allocation scheme. 
# Change values and see portfolio change accordingly
capital_to_invest = 100000
buy_date = '2021-12-31'
scheme = 'Low Risk'

weights_dict = get_scheme_weights(alloc_df, scheme = 'Low Risk')  
positions_df = open_portfolio_positions(close_df, buy_date, weights_dict, capital_to_invest)
positions_df

# see how portfolio has changed since the open date by used a future date

cur_date = '2022-12-30'
positions_df = portfolio_value_on_date(positions_df, close_df, cur_date)
positions_df

# close the positions as part of the next rebalancing process
sell_date = '2022-12-30'
closed_positions_df = close_portfolio_positions(positions_df, close_df, sell_date)
closed_positions_df

# get a visual of how the individual instruments performed between a start date and a end date

normalized = normalized_returns_between_dates(close_df,start_date=buy_date,end_date=sell_date)
plot_normalized_returns(normalized, figsize=(9,6))

The code implements a subset of building blocks and functions that could be used as part of a backtest of Katsanos’ strategy. The user can use the code to test and assess various performance aspects of the allocation strategy offered in the article. The code can be used to test different symbol sets and test different allocation schemes.

Example usages of the code include:

  1. To open a portfolio using the allocation schemes offered
  2. To see portfolio performance on any given date
  3. To close a position and open a new set of positions (such as to rebalance the portfolio)
  4. To plot individual ticker performances between any two dates using the plotter function
  5. Many other usages are also available once details of the building blocks are understood.
Sample Chart

FIGURE 3: PYTHON. Shown here is an example plot of returns from a test of the allocation strategy. This type of plot helps the user see how individual instruments performed in a given date range when the user enters a start and end date.

A Jupyter notebook for this and previous Traders’ Tips articles can be found on GitHub at: https://github.com/jainraje/TraderTipArticles.

—Rajeev Jain
jainraje@yahoo.com

BACK TO LIST

logo

NeuroShell Trader: February 2026

Markos Katsanos’ article in this issue, “A Portfolio Diversification Strategy,” highlights a simple reality: what keeps investors up at night isn’t day-to-day volatility—it’s the depth and duration of drawdowns. While NeuroShell Trader doesn’t implement the article’s rolling, dynamic allocation model directly, it does provide a practical way to pursue the same goal: improving risk-adjusted performance by optimizing trading systems across a portfolio of instruments using objective functions that directly measure drawdown stress.

NeuroShell Trader lets you optimize a trading system using a wide set of objective functions—including Sharpe ratio, ulcer index, ulcer performance index, and many others—so you can explicitly target what matters most for your risk profile, whether that is return, equity-curve smoothness, drawdown severity, or a combination of these. This is accomplished by simply selecting the desired optimization goal from the optimization tab of the trading strategy wizard when creating a trading system.

Where NeuroShell Trader truly excels for multi-asset strategies is its ability to optimize across chart pages (multiple symbols). Instead of tuning parameters that perform well on a single ETF but degrade elsewhere, you can optimize across a set of instruments representing your intended portfolio and have NeuroShell Trader search for parameters that maximize the average objective value across the entire group. In effect, this allows traders to optimize explicitly for portfolio robustness—not just single-market performance—using the same drawdown-aware risk metrics highlighted in the article, such as the Sharpe ratio and ulcer index. This capability is enabled by selecting “optimize across chart pages” on the optimization tab of the trading strategy wizard.

—Ward Systems Group, Inc.
sales@wardsystems.com
www.neuroshell.com

BACK TO LIST

logo

NinjaTrader: February 2026

In “A Portfolio Diversification Strategy,” in this issue, Markos Katsanos presents some concepts related to portfolio diversification and asset allocation.

Files for a NinjaTrader indicator and a NinjaTrader strategy, for helping to implement or explore some of the author’s logic described in his article, are available for download at the following link for NinjaTrader 8:

Once the files are downloaded, you can import them into NinjaTrader 8 from within the control center by selecting Tools → Import → NinjaScript Add-On and then selecting the downloaded files for NinjaTrader 8.

You can review the source code in NinjaTrader 8 by selecting New → NinjaScript Editor → Indicators from within the control center window and selecting the file.

Example charts in NinjaTrader are shown in Figures 4 and 5. Figure 4 demonstrates a statistical exploration, and Figure 5 demonstrates an asset allocation approach with rebalancing using a set of symbols.

Sample Chart

FIGURE 4: NINJATRADER. A statistical exploration is demonstrated on a weekly chart of the ETF GLD over about a decade of data.

Sample FIGURE

FIGURE 5: NINJATRADER. An approach to asset allocation is demonstrated on a weekly chart of emini S&P 500 futures (ES) over about a decade of data.

NinjaScript uses compiled DLLs that run native, not interpreted, to provide you with the highest performance possible.

—NinjaTrader_JesseN
NinjaTrader, LLC
www.ninjatrader.com

BACK TO LIST

Originally published in the February 2026 issue of
Technical Analysis of STOCKS & COMMODITIES magazine.
All rights reserved. © Copyright 2025, Technical Analysis, Inc.