Every feature has a timestamp: leakage, scaling and twin features

On this page
  1. The leak
  2. Scaling: units, not information
  3. Twin features: ridge shares, lasso chooses
  4. A rolling z-score hides a crash
  5. Traps
  6. The code
  7. Summary
  8. References

A model is only as honest as its inputs. This post builds a dishonest input on purpose, shows how it looks in the score and how to catch it, and then covers the two quieter feature problems: units and twins.

a coefficient or a chosen penaltyerrorthe leak

The leak

The series’ seven features are all computed from a ticker’s prices up to the day of the forecast: recent returns, recent volatility, distance from the moving average. I added an eighth on purpose: SPY’s volatility over the same month the model is trying to predict. It is a perfectly reasonable-looking column, the kind that appears when a market-wide table is joined onto a per-ticker table by month without thinking about which month. The score is the test RMSE, the typical size of a miss in points of volatility; lower is better (Table 1).

Table 1. Test RMSE with the honest features, the leak, and the lagged market feature
FeaturesTest RMSE
Seven honest features12.78
Plus SPY’s volatility over the same month (not knowable yet)11.19\bad{11.19}
Plus SPY’s volatility over the last month (knowable)12.48

The leak looks like the best model in Table 1. The honest version of the same idea, last month’s market volatility, adds a third of a point. Nothing in the score distinguishes the two; the only difference is a timestamp.

{"panels":[{"x0":45.8,"x1":564.91,"y0":29.16,"y1":177.33,"title":"1. The leak test: lag the suspicious feature and watch the score","xlabel":"Feature set","ylabel":"Test RMSE (volatility points)","xlog":false},{"x0":45.8,"x1":564.91,"y0":352.09,"y1":500.27,"title":"2. Ridge shrinks every coefficient toward zero; lasso drives some exactly to zero","xlabel":"Penalty strength alpha (log scale)","ylabel":"Standardised coefficient","xlog":true},{"x0":45.8,"x1":564.91,"y0":675.03,"y1":823.2,"title":"3. A rolling z-score hides a crash: the window's spread balloons with the fall","xlabel":"Date","ylabel":"z-score","xlog":false}],"series":[{"panel":1,"label":"ret_1d (lasso)","color":"var(--chart-maroon, #b6262c)","kind":"line","x":[0.001,0.0013434,0.00180472,0.00242446,0.00325702,0.00437548,0.00587802,0.00789652,0.0106082,0.014251,0.0191448,0.0257191,0.0345511,0.0464159,0.0623551,0.0837678,0.112534,0.151178,0.203092,0.272833,0.366524,0.492388,0.661474,0.888624,1.19378,1.60372,2.15443,2.89427,3.88816,5.22335,7.01704,9.42668,12.6638,17.0125,22.8546,30.7029,41.2463,55.4102,74.438,100.0],"y":[-0.529637,-0.529475,-0.529258,-0.528966,-0.528573,-0.528044,-0.527334,-0.52638,-0.525098,-0.523384,-0.521069,-0.517956,-0.513769,-0.508133,-0.500543,-0.492497,-0.488279,-0.482609,-0.474799,-0.430641,-0.362286,-0.268883,-0.143405,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"sx":[69.4,81.5,93.6,105.7,117.8,129.9,142.0,154.1,166.2,178.3,190.4,202.5,214.6,226.7,238.81,250.91,263.01,275.11,287.21,299.31,311.41,323.51,335.61,347.71,359.81,371.91,384.01,396.11,408.21,420.31,432.41,444.51,456.61,468.71,480.82,492.92,505.02,517.12,529.22,541.32],"sy":[468.52,468.52,468.52,468.51,468.51,468.5,468.49,468.48,468.46,468.44,468.41,468.38,468.32,468.26,468.16,468.06,468.01,467.94,467.84,467.3,466.46,465.3,463.76,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99]},{"panel":1,"label":"ret_5d (lasso)","color":"var(--chart-blue, #3b6ea5)","kind":"line","x":[0.001,0.0013434,0.00180472,0.00242446,0.00325702,0.00437548,0.00587802,0.00789652,0.0106082,0.014251,0.0191448,0.0257191,0.0345511,0.0464159,0.0623551,0.0837678,0.112534,0.151178,0.203092,0.272833,0.366524,0.492388,0.661474,0.888624,1.19378,1.60372,2.15443,2.89427,3.88816,5.22335,7.01704,9.42668,12.6638,17.0125,22.8546,30.7029,41.2463,55.4102,74.438,100.0],"y":[-0.887105,-0.887762,-0.88866,-0.889849,-0.891446,-0.893589,-0.896464,-0.900321,-0.905495,-0.912544,-0.921877,-0.934383,-0.951126,-0.973509,-1.00336,-1.04765,-1.12076,-1.21894,-1.34747,-1.34524,-1.28644,-1.19319,-1.06792,-0.888766,-0.564823,-0.129638,-0.0,-0.0,-0.0,-0.0,-0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"sx":[69.4,81.5,93.6,105.7,117.8,129.9,142.0,154.1,166.2,178.3,190.4,202.5,214.6,226.7,238.81,250.91,263.01,275.11,287.21,299.31,311.41,323.51,335.61,347.71,359.81,371.91,384.01,396.11,408.21,420.31,432.41,444.51,456.61,468.71,480.82,492.92,505.02,517.12,529.22,541.32],"sy":[472.93,472.94,472.95,472.96,472.98,473.01,473.04,473.09,473.16,473.24,473.36,473.51,473.72,473.99,474.36,474.91,475.81,477.02,478.61,478.58,477.85,476.7,475.16,472.95,468.95,463.59,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99]},{"panel":1,"label":"ret_20d (lasso)","color":"var(--chart-grey, #a3a19c)","kind":"line","x":[0.001,0.0013434,0.00180472,0.00242446,0.00325702,0.00437548,0.00587802,0.00789652,0.0106082,0.014251,0.0191448,0.0257191,0.0345511,0.0464159,0.0623551,0.0837678,0.112534,0.151178,0.203092,0.272833,0.366524,0.492388,0.661474,0.888624,1.19378,1.60372,2.15443,2.89427,3.88816,5.22335,7.01704,9.42668,12.6638,17.0125,22.8546,30.7029,41.2463,55.4102,74.438,100.0],"y":[1.81052,1.80742,1.80323,1.79763,1.79011,1.78001,1.76645,1.74823,1.72378,1.69076,1.64664,1.58743,1.50799,1.40146,1.25874,1.07168,0.832728,0.511794,0.0866834,0.0,0.0,0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"sx":[69.4,81.5,93.6,105.7,117.8,129.9,142.0,154.1,166.2,178.3,190.4,202.5,214.6,226.7,238.81,250.91,263.01,275.11,287.21,299.31,311.41,323.51,335.61,347.71,359.81,371.91,384.01,396.11,408.21,420.31,432.41,444.51,456.61,468.71,480.82,492.92,505.02,517.12,529.22,541.32],"sy":[439.66,439.7,439.75,439.82,439.91,440.04,440.21,440.43,440.73,441.14,441.68,442.41,443.39,444.71,446.47,448.77,451.72,455.68,460.92,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99]},{"panel":1,"label":"vol_5d (lasso)","color":"var(--chart-purple, #7c5fd3)","kind":"line","x":[0.001,0.0013434,0.00180472,0.00242446,0.00325702,0.00437548,0.00587802,0.00789652,0.0106082,0.014251,0.0191448,0.0257191,0.0345511,0.0464159,0.0623551,0.0837678,0.112534,0.151178,0.203092,0.272833,0.366524,0.492388,0.661474,0.888624,1.19378,1.60372,2.15443,2.89427,3.88816,5.22335,7.01704,9.42668,12.6638,17.0125,22.8546,30.7029,41.2463,55.4102,74.438,100.0],"y":[5.58171,5.58185,5.58204,5.5823,5.58264,5.58309,5.58371,5.58453,5.58563,5.58714,5.58912,5.59177,5.59531,5.60003,5.60629,5.61295,5.61692,5.62225,5.62975,5.61108,5.57493,5.52262,5.45236,5.36059,5.25779,5.11969,4.84185,4.43355,3.88505,3.14819,2.15829,0.828462,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"sx":[69.4,81.5,93.6,105.7,117.8,129.9,142.0,154.1,166.2,178.3,190.4,202.5,214.6,226.7,238.81,250.91,263.01,275.11,287.21,299.31,311.41,323.51,335.61,347.71,359.81,371.91,384.01,396.11,408.21,420.31,432.41,444.51,456.61,468.71,480.82,492.92,505.02,517.12,529.22,541.32],"sy":[393.16,393.16,393.15,393.15,393.15,393.14,393.13,393.12,393.11,393.09,393.07,393.03,392.99,392.93,392.86,392.77,392.72,392.66,392.57,392.8,393.24,393.89,394.75,395.89,397.15,398.86,402.28,407.32,414.08,423.17,435.37,451.77,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99]},{"panel":1,"label":"vol_20d (lasso)","color":"var(--chart-amber, #c0781a)","kind":"line","x":[0.001,0.0013434,0.00180472,0.00242446,0.00325702,0.00437548,0.00587802,0.00789652,0.0106082,0.014251,0.0191448,0.0257191,0.0345511,0.0464159,0.0623551,0.0837678,0.112534,0.151178,0.203092,0.272833,0.366524,0.492388,0.661474,0.888624,1.19378,1.60372,2.15443,2.89427,3.88816,5.22335,7.01704,9.42668,12.6638,17.0125,22.8546,30.7029,41.2463,55.4102,74.438,100.0],"y":[8.36426,8.36372,8.36301,8.36204,8.36075,8.35902,8.35669,8.35357,8.34937,8.34371,8.33614,8.32597,8.31233,8.29403,8.2695,8.23978,8.20936,8.1685,8.1132,8.05423,7.98146,7.88607,7.75793,7.58406,7.33704,7.00519,6.67,6.2618,5.71342,4.97674,3.98707,2.65756,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"sx":[69.4,81.5,93.6,105.7,117.8,129.9,142.0,154.1,166.2,178.3,190.4,202.5,214.6,226.7,238.81,250.91,263.01,275.11,287.21,299.31,311.41,323.51,335.61,347.71,359.81,371.91,384.01,396.11,408.21,420.31,432.41,444.51,456.61,468.71,480.82,492.92,505.02,517.12,529.22,541.32],"sy":[358.85,358.85,358.86,358.87,358.89,358.91,358.94,358.98,359.03,359.1,359.19,359.32,359.49,359.71,360.01,360.38,360.76,361.26,361.94,362.67,363.57,364.74,366.32,368.47,371.51,375.61,379.74,384.77,391.53,400.62,412.82,429.22,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99]},{"panel":1,"label":"sma_ratio (lasso)","color":"var(--chart-green, #3f8f45)","kind":"line","x":[0.001,0.0013434,0.00180472,0.00242446,0.00325702,0.00437548,0.00587802,0.00789652,0.0106082,0.014251,0.0191448,0.0257191,0.0345511,0.0464159,0.0623551,0.0837678,0.112534,0.151178,0.203092,0.272833,0.366524,0.492388,0.661474,0.888624,1.19378,1.60372,2.15443,2.89427,3.88816,5.22335,7.01704,9.42668,12.6638,17.0125,22.8546,30.7029,41.2463,55.4102,74.438,100.0],"y":[-2.54265,-2.53745,-2.53042,-2.52102,-2.5084,-2.49144,-2.46868,-2.4381,-2.39705,-2.34166,-2.26759,-2.16815,-2.03471,-1.8557,-1.61576,-1.32727,-1.03916,-0.652227,-0.140152,-0.0149615,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"sx":[69.4,81.5,93.6,105.7,117.8,129.9,142.0,154.1,166.2,178.3,190.4,202.5,214.6,226.7,238.81,250.91,263.01,275.11,287.21,299.31,311.41,323.51,335.61,347.71,359.81,371.91,384.01,396.11,408.21,420.31,432.41,444.51,456.61,468.71,480.82,492.92,505.02,517.12,529.22,541.32],"sy":[493.34,493.28,493.19,493.08,492.92,492.71,492.43,492.05,491.55,490.87,489.95,488.73,487.08,484.87,481.91,478.36,474.8,470.03,463.72,462.17,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99]},{"panel":1,"label":"bollinger (lasso)","color":"var(--chart-maroon, #b6262c)","kind":"line","x":[0.001,0.0013434,0.00180472,0.00242446,0.00325702,0.00437548,0.00587802,0.00789652,0.0106082,0.014251,0.0191448,0.0257191,0.0345511,0.0464159,0.0623551,0.0837678,0.112534,0.151178,0.203092,0.272833,0.366524,0.492388,0.661474,0.888624,1.19378,1.60372,2.15443,2.89427,3.88816,5.22335,7.01704,9.42668,12.6638,17.0125,22.8546,30.7029,41.2463,55.4102,74.438,100.0],"y":[0.480091,0.477926,0.475016,0.471108,0.465858,0.458805,0.449331,0.436604,0.419507,0.396532,0.365677,0.324229,0.268552,0.193763,0.0933085,0.0,0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,-0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"sx":[69.4,81.5,93.6,105.7,117.8,129.9,142.0,154.1,166.2,178.3,190.4,202.5,214.6,226.7,238.81,250.91,263.01,275.11,287.21,299.31,311.41,323.51,335.61,347.71,359.81,371.91,384.01,396.11,408.21,420.31,432.41,444.51,456.61,468.71,480.82,492.92,505.02,517.12,529.22,541.32],"sy":[456.07,456.1,456.13,456.18,456.24,456.33,456.45,456.61,456.82,457.1,457.48,457.99,458.68,459.6,460.84,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99,461.99]},{"panel":2,"label":"SPY price z-score, 60-day window","color":"var(--chart-maroon, #b6262c)","kind":"line","x":["2025-01-02","2025-01-03","2025-01-06","2025-01-07","2025-01-08","2025-01-10","2025-01-13","2025-01-14","2025-01-15","2025-01-16","2025-01-17","2025-01-21","2025-01-22","2025-01-23","2025-01-24","2025-01-27","2025-01-28","2025-01-29","2025-01-30","2025-01-31","2025-02-03","2025-02-04","2025-02-05","2025-02-06","2025-02-07","2025-02-10","2025-02-11","2025-02-12","2025-02-13","2025-02-14","2025-02-18","2025-02-19","2025-02-20","2025-02-21","2025-02-24","2025-02-25","2025-02-26","2025-02-27","2025-02-28","2025-03-03","2025-03-04","2025-03-05","2025-03-06","2025-03-07","2025-03-10","2025-03-11","2025-03-12","2025-03-13","2025-03-14","2025-03-17","2025-03-18","2025-03-19","2025-03-20","2025-03-21","2025-03-24","2025-03-25","2025-03-26","2025-03-27","2025-03-28","2025-03-31","2025-04-01","2025-04-02","2025-04-03","2025-04-04","2025-04-07","2025-04-08","2025-04-09","2025-04-10","2025-04-11","2025-04-14","2025-04-15","2025-04-16","2025-04-17","2025-04-21","2025-04-22","2025-04-23","2025-04-24","2025-04-25","2025-04-28","2025-04-29","2025-04-30","2025-05-01","2025-05-02","2025-05-05","2025-05-06","2025-05-07","2025-05-08","2025-05-09","2025-05-12","2025-05-13","2025-05-14","2025-05-15","2025-05-16","2025-05-19","2025-05-20","2025-05-21","2025-05-22","2025-05-23","2025-05-27","2025-05-28","2025-05-29","2025-05-30","2025-06-02","2025-06-03","2025-06-04","2025-06-05","2025-06-06","2025-06-09","2025-06-10","2025-06-11","2025-06-12","2025-06-13","2025-06-16","2025-06-17","2025-06-18","2025-06-20","2025-06-23","2025-06-24","2025-06-25","2025-06-26","2025-06-27","2025-06-30","2025-07-01","2025-07-02","2025-07-03","2025-07-07","2025-07-08","2025-07-09","2025-07-10","2025-07-11","2025-07-14","2025-07-15","2025-07-16","2025-07-17","2025-07-18","2025-07-21","2025-07-22","2025-07-23","2025-07-24","2025-07-25","2025-07-28","2025-07-29","2025-07-30","2025-07-31","2025-08-01","2025-08-04","2025-08-05","2025-08-06","2025-08-07","2025-08-08","2025-08-11","2025-08-12","2025-08-13","2025-08-14","2025-08-15","2025-08-18","2025-08-19","2025-08-20","2025-08-21","2025-08-22","2025-08-25","2025-08-26","2025-08-27","2025-08-28","2025-08-29","2025-09-02","2025-09-03","2025-09-04","2025-09-05","2025-09-08","2025-09-09","2025-09-10","2025-09-11","2025-09-12","2025-09-15","2025-09-16","2025-09-17","2025-09-18","2025-09-19","2025-09-22","2025-09-23","2025-09-24","2025-09-25","2025-09-26","2025-09-29","2025-09-30"],"y":[-0.398497,0.238775,0.530736,-0.126697,-0.0640483,-0.930778,-0.854594,-0.781848,0.232187,0.10598,0.668054,1.16874,1.45442,1.71347,1.48865,0.621711,1.09641,0.799291,1.12536,0.768964,0.273127,0.760224,1.04743,1.27973,0.54512,1.04785,1.0744,0.804164,1.54751,1.49809,1.66007,1.76308,1.39946,0.104449,-0.240695,-0.604799,-0.557814,-1.66344,-0.55043,-1.72107,-2.36238,-1.60261,-2.5389,-2.0858,-3.23291,-3.2523,-2.76072,-3.01569,-2.07888,-1.70184,-2.00518,-1.55541,-1.60693,-1.53621,-0.886276,-0.775813,-1.15745,-1.20867,-1.81224,-1.53757,-1.40213,-1.16469,-2.51712,-3.61128,-3.24739,-3.19678,-1.1755,-1.97574,-1.56902,-1.33532,-1.33823,-1.67856,-1.58734,-1.91561,-1.42208,-1.1161,-0.722284,-0.569422,-0.538704,-0.401426,-0.373602,-0.219613,0.0858693,-0.00336694,-0.148148,-0.0396707,0.12883,0.127553,0.869143,1.05297,1.12281,1.28344,1.4745,1.50984,1.43085,1.02193,1.04786,0.881506,1.4061,1.24762,1.32243,1.28014,1.38523,1.48705,1.42924,1.26939,1.4573,1.42318,1.4975,1.38641,1.42261,1.14382,1.30588,1.09149,1.06471,0.995963,1.16656,1.34866,1.31248,1.41683,1.45968,1.49569,1.43553,1.49364,1.62207,1.46598,1.40193,1.49032,1.5044,1.38465,1.38503,1.26825,1.32225,1.47135,1.44431,1.47532,1.44798,1.63577,1.60743,1.68176,1.63646,1.5239,1.44451,1.3032,0.794229,1.23936,1.05755,1.2946,1.22787,1.44699,1.33331,1.63769,1.68421,1.62007,1.48498,1.43738,1.22201,1.10912,0.938442,1.4478,1.25347,1.37482,1.42207,1.51085,1.24681,0.935154,1.11944,1.41845,1.2619,1.32864,1.38657,1.48487,1.79677,1.75504,1.96895,1.89852,1.81694,1.99431,2.17565,2.31059,1.95859,1.73544,1.46182,1.6831,1.74644,1.85577],"sx":[69.4,71.14,76.36,78.11,79.85,83.33,88.55,90.3,92.04,93.78,95.52,102.48,104.23,105.97,107.71,112.93,114.67,116.42,118.16,119.9,125.12,126.86,128.61,130.35,132.09,137.31,139.05,140.8,142.54,144.28,151.24,152.99,154.73,156.47,161.69,163.43,165.18,166.92,168.66,173.88,175.62,177.37,179.11,180.85,186.07,187.81,189.55,191.3,193.04,198.26,200.0,201.74,203.49,205.23,210.45,212.19,213.93,215.68,217.42,222.64,224.38,226.12,227.87,229.61,234.83,236.57,238.31,240.06,241.8,247.02,248.76,250.5,252.25,259.21,260.95,262.69,264.44,266.18,271.4,273.14,274.88,276.62,278.37,283.59,285.33,287.07,288.81,290.56,295.78,297.52,299.26,301.0,302.75,307.97,309.71,311.45,313.19,314.94,321.9,323.64,325.38,327.13,332.35,334.09,335.83,337.57,339.32,344.54,346.28,348.02,349.76,351.5,356.73,358.47,360.21,363.69,368.92,370.66,372.4,374.14,375.88,381.11,382.85,384.59,386.33,393.3,395.04,396.78,398.52,400.26,405.49,407.23,408.97,410.71,412.45,417.68,419.42,421.16,422.9,424.64,429.87,431.61,433.35,435.09,436.83,442.06,443.8,445.54,447.28,449.02,454.25,455.99,457.73,459.47,461.21,466.44,468.18,469.92,471.66,473.4,478.63,480.37,482.11,483.85,485.59,492.56,494.3,496.04,497.78,503.01,504.75,506.49,508.23,509.97,515.2,516.94,518.68,520.42,522.16,527.39,529.13,530.87,532.61,534.35,539.58,541.32],"sy":[743.38,728.89,722.25,737.2,735.78,755.49,753.76,752.1,729.04,731.91,719.12,707.73,701.24,695.34,700.46,720.18,709.38,716.14,708.72,716.83,728.11,717.03,710.49,705.21,721.92,710.48,709.88,716.03,699.12,700.24,696.56,694.22,702.49,731.94,739.79,748.08,747.01,772.16,746.84,773.47,788.06,770.77,792.07,781.76,807.86,808.3,797.12,802.92,781.61,773.03,779.93,769.7,770.87,769.26,754.48,751.97,760.65,761.81,775.54,769.29,766.21,760.81,791.58,816.46,808.19,807.04,761.06,779.26,770.01,764.69,764.76,772.5,770.43,777.89,766.67,759.71,750.75,747.27,746.57,743.45,742.82,739.32,732.37,734.4,737.69,735.22,731.39,731.42,714.55,710.37,708.78,705.13,700.78,699.98,701.77,711.07,710.48,714.27,702.34,705.94,704.24,705.2,702.81,700.49,701.81,705.45,701.17,701.95,700.26,702.78,701.96,708.3,704.62,709.49,710.1,711.66,707.78,703.64,704.46,702.09,701.12,700.3,701.67,700.34,697.42,700.97,702.43,700.42,700.1,702.82,702.81,705.47,704.24,700.85,701.47,700.76,701.38,697.11,697.76,696.07,697.1,699.66,701.46,704.68,716.25,706.13,710.26,704.87,706.39,701.41,703.99,697.07,696.01,697.47,700.54,701.62,706.52,709.09,712.97,701.39,705.81,703.05,701.97,699.95,705.96,713.05,708.86,702.05,705.62,704.1,702.78,700.54,693.45,694.4,689.53,691.13,692.99,688.96,684.83,681.76,689.77,694.84,701.07,696.03,694.59,692.11]},{"panel":2,"label":"z = \u22122","color":"var(--chart-blue, #3b6ea5)","kind":"line","x":["1970-01-01","1970-01-02"],"y":[-2.0,-2.0],"sx":[-34915.31,-34913.57],"sy":[779.81,779.81]}]} 2026-10-31T01:55:20.179063 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/ seven honest features plus SPY's same-month volatility (leak) the leak, lagged 20 days plus SPY's last-month volatility (honest) Feature set 0 2 4 6 8 10 12 Test RMSE (volatility points) 12.78 11.19 12.40 12.48 1. The leak test: lag the suspicious feature and watch the score 1 0 − 3 1 0 − 2 1 0 − 1 1 0 0 1 0 1 1 0 2 Penalty strength alpha (log scale) −2 0 2 4 6 8 Standardised coefficient 2. Ridge shrinks every coefficient toward zero; lasso drives some exactly to zero ret_1d (lasso) ret_5d (lasso) ret_20d (lasso) vol_5d (lasso) vol_20d (lasso) sma_ratio (lasso) bollinger (lasso) dotted: the same feature under ridge 2025-01 2025-02 2025-03 2025-04 2025-05 2025-06 2025-07 2025-08 2025-09 2025-10 Date −3 −2 −1 0 1 2 z-score 3. A rolling z-score hides a crash: the window's spread balloons with the fall SPY price z-score, 60-day window z = −2
Figure 1. Test error by feature set, the ridge and lasso paths, and SPY's z-score in 2025

How to catch it. Lag the suspicious feature by one period and rerun. The leaked column shifted back by twenty trading days scores 12.40 (the third bar in Figure 1), next to the honest feature’s 12.48 and nowhere near 11.19: most of the “signal” vanished, which means it was never a signal about the future, only a copy of it. Removing the feature is the same test. Run either on any result that looks too good.

Leakage comes in two shapes. Time leakage uses the future: a same-month statistic, a revised figure, a label that overlaps the test period. Pooling leakage uses the test rows: a mean, a scaler or a feature-selection step computed over the whole table. A penalty on the model’s weights (ridge and lasso, below) does not stop either, and a correct split cannot repair a feature that was built looking forward.

Scaling: units, not information

Some models measure distances between rows (nearest neighbours), or penalise the size of their coefficients, the weights a linear model gives each feature (ridge, lasso, logistic regression). Both depend on the units of each feature, so features are standardised: each column rescaled to a mean of zero and a spread of one. Scaling changes a feature’s influence, not its information. Trees do not need it, because a split only asks whether a value is above or below a threshold.

The rule that matters is where the scaler is fitted. Fit it on the training rows and apply it to the test rows. Fitted on all rows together, it carries the test set’s mean into the model: in this data the pooled mean of 20-day volatility is 28.53 points and the training-only mean is 29.94, because the pooled version already knows 2024 and 2025 were calmer than 2020 to 2023. The difference is small here; the habit is the point, and scikit-learn’s Pipeline exists so that the habit is automatic.

Twin features: ridge shares, lasso chooses

Add a column that is 1.02 times the 20-day volatility plus a hair of noise, a twin of a feature already there. Then fit three models. Least squares is the plain linear model. Ridge and lasso are the same model with a penalty added to the total it minimises, a cost for having large weights, so the fit keeps the weights small unless they earn their place. The penalty’s strength, written λ\lambda (lambda), is a setting you choose (Table 2):

Table 2. Coefficients on twin features under least squares, ridge and lasso
ModelCoefficient on vol_20dOn its twinTheir sumTest RMSE
Ordinary least squares−65.77\risk{-65.77}+74.15\risk{+74.15}+8.3812.78
Ridge, penalty 1−5.14+13.52+8.3712.78
Lasso, penalty 0.050.00\key{0.00}+8.30+8.3012.76

Least squares gives the twins wildly opposite weights that cancel; the predictions are fine, the coefficients are meaningless, and a tiny change in the data would swap them. Ridge pulls both a long way towards zero; in this data they keep opposite signs, at a fraction of the size. Lasso drops one exactly. The sum is stable in every row; the split between the twins is not. Correlated features break the interpretation of a linear model before they break its predictions, and in a random forest they split the importance score between them without breaking anything.

The two penalties side by side (Table 3):

Table 3. Ridge against lasso
ItemRidge (L2)Lasso (L1)
Adds to the lossλ∑βj2\lambda \sum \key{\beta_j^2}λ∑∣βj∣\lambda \sum \lvert \key{\beta_j} \rvert
Effect as λ\lambda growsevery coefficient shrinks, none reaches zerosome reach exactly zero
With twinsshares the weightpicks one
Use whenmany small effectsa few real effects among many candidates

The second panel of Figure 1 draws both paths on the seven honest features as the penalty grows: lasso’s lines hit zero one by one (six features left at a penalty of 0.1, three at 1), ridge’s lines approach zero and never arrive. The penalty is chosen on validation data, rows held back from training for picking settings like this. At zero it is plain least squares; large enough, it predicts the mean.

A rolling z-score hides a crash

A common feature is a rolling z-score: how far today’s price sits from its recent average, in recent standard deviations. It has a flaw that shows up exactly when it matters (Table 4).

Table 4. The same six-point gap as a z-score in a calm and a wild window
Window meanWindow sdPricez
10021063.0
100101060.6

The same six-point gap reads as extreme in a calm window and as nothing in a wild one, because the wild window’s standard deviation has ballooned. SPY in the spring of 2025: a week into the fall, on 27 February, the 60-day z-score read −1.66. At the bottom on 8 April, with the price 83 points below the window’s mean, it read −3.20, but only because the window’s standard deviation had tripled from 8.1 to 25.8 along the way; measured against the calm February window it would have read near −10. The third panel of Figure 1 shows the z-score dropping past −3 at the bottom of the worst fall of the window and then snapping back above zero within weeks. A feature built to flag extremes undersold the biggest one, and said nothing about how fast it would end.

Traps

Things people say about features, and what is true (Table 5):

Table 5. Traps about leakage, scaling and twin features
TrapTruth
Leakage is only about timePooling the test rows leaks too
Future returns as the label are leakageThat is the target; leakage is about the inputs
Trees need scalingNo
Correlated features break a random forestThey dilute its importance scores; they break least-squares coefficients
Lasso picks the truly important twinIt picks one, arbitrarily
Fewer features is always betterA simpler model is steadier on new data but can miss real effects; bias and variance covers the trade
A z-score is capped at ±3It is not, and in a crash it shrinks

The code

All four ideas on made-up data, so it runs as pasted (pip install numpy scikit-learn); scaling is the StandardScaler step in the twins part. It builds a daily series whose volatility drifts slowly, like real volatility. past_20d is the honest feature (the last 20 days’ volatility, knowable today) and y is the answer (the next 20 days’ volatility). The first 800 rows train the model and the rest test it, in time order.

import numpy as np
from sklearn.linear_model import Lasso, LinearRegression, Ridge
from sklearn.metrics import root_mean_squared_error
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

# Made-up daily returns whose volatility drifts slowly, as real volatility does
rng = np.random.default_rng(0)
n = 1200
vol = np.exp(np.cumsum(rng.normal(0, 0.05, n)))
returns = rng.normal(0, vol)
days = range(40, n - 20)
past_20d = np.array([returns[t - 20:t].std() for t in days])   # knowable today
next_20d = np.array([returns[t:t + 20].std() for t in days])   # the answer: not knowable yet
y = next_20d
train, test = slice(0, 800), slice(800, None)                  # test later than training

# 1. The leak: a column computed over the same days as the answer
leak = next_20d + rng.normal(0, 0.02, len(y))                  # a copy of the future, with noise
leak_lagged = np.r_[np.full(20, leak[0]), leak[:-20]]          # the same column, 20 days older
for name, X in [("honest feature only       ", np.c_[past_20d]),
                ("plus the leak             ", np.c_[past_20d, leak]),
                ("plus the leak, lagged 20 d", np.c_[past_20d, leak_lagged])]:
    model = LinearRegression().fit(X[train], y[train])
    print(name, "test RMSE", round(root_mean_squared_error(y[test], model.predict(X[test])), 3))

# 2. Twin features: a second column that says almost the same thing as the first
twin = 1.02 * past_20d + rng.normal(0, 0.001, len(y))
X = np.c_[past_20d, twin]
for name, model in [("least squares", LinearRegression()),
                    ("ridge        ", Ridge(alpha=1.0)),
                    ("lasso        ", Lasso(alpha=0.01))]:
    pipe = make_pipeline(StandardScaler(), model).fit(X[train], y[train])  # scaler sees training rows only
    w = pipe[-1].coef_                                         # the weights, one per column
    print(name, "weights", np.round(w, 3), "sum", round(w.sum(), 3))

# 3. A rolling z-score: the same 6-point gap, in a calm window and in a wild one
for mean, sd, price in [(100, 2, 106), (100, 10, 106)]:
    print(f"window mean {mean}, sd {sd}, price {price}: z = {(price - mean) / sd:.1f}")

It prints:

  • The leak: the leaked column cuts the test miss from about 0.083 to 0.020, a model that looks far better. Lagged by 20 days it scores 0.084, no better than without it: it was a copy of the answer, not a signal.
  • Twins: least squares gives the twins opposite weights (about −0.81 and +1.18), ridge splits the weight evenly (0.18 and 0.185), lasso keeps one and sets the other to exactly 0. The sum stays about the same in every row, as in Table 2.
  • z-scores: 3.0 in the calm window, 0.6 in the wild one, the two rows of Table 4.

np.c_[a, b] puts columns side by side into a table, and pipe[-1] is the last step of the pipeline, the model itself.

The script behind the real numbers

The tables above come from examples/every-feature-has-a-timestamp/leakage.py, which runs the same tests on the blog’s five tickers. To run it yourself, clone the examples repo: its README has a three-command Docker setup, or run uv run every-feature-has-a-timestamp/leakage.py.

Summary

Table 6 is the post in six rows.

Table 6. The post’s six ideas, each with its rule
IdeaRuleRemember it as
Timestampwhen was this knowable?lag everything to the last close
The leak testlag or remove the suspect, reruna real signal survives a lag
Scalingstandardise, fitted on training rowsunits, not information
Ridgeλ∑β2\lambda \sum \beta^2shrinks all, shares twins
Lassoλ∑∣β∣\lambda \sum \lvert \beta \rvertzeroes some, picks a twin
Rolling z-score(price − mean) / sdgoes quiet in a crash

References

Tagged machine-learning, finance, volatility, regression← Home