01 The Hypothesis
Can a convolutional neural network learn to predict short-term price direction by recognizing visual patterns in a GAF image?
A GAF matrix is a compact fingerprint of a price series' internal temporal structure โ two windows with similar momentum, reversal, and consolidation dynamics produce visually similar matrices. If patterns that look like today's consistently preceded a specific outcome, a CNN should learn the association.
Target: given the current 120-bar GAF, output a statement like "80% probability this market moves up 2โ5% over the next 5 bars."
price series (120 bars)
โ
โผ
GAF transform
โ
โผ
120ร120 matrix "image"
โ
โผ
CNN classifier
โ
โผ
"80% probability this market
moves +2โ5% over next 5 bars"
02 What Is a GAF?
A Gramian Angular Field converts a 1-D time series into a 2-D matrix that encodes temporal correlation: each cell [i, j] expresses the relationship between the price at time i and time j. The result is image-shaped โ exactly what CNNs are built to consume.
The variant used is the Gramian Angular Summation Field: result[i,j] = cos(phi_i + phi_j). Four steps, implemented in GAF.cs:
- Min-max scale the series to [-1, 1]
- Clamp floating-point overshoot so acos stays valid
- Arc-cosine each value: phi[i] = acos(scaled[i])
- Build the NรN matrix: cos(phi[i] + phi[j])
float[] series (N values)
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Min-Max scale to [-1, 1] โ
โ โ
โ min = series.Min() โ
โ max = series.Max() โ
โ scaled[i] = (2*x[i] - max - min) โ
โ / (max - min) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Clamp to [-1, 1] โ
โ โ
โ if scaled[i] > 1.0 โ set 1.0 โ
โ if scaled[i] < -1.0 โ set -1.0 โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Arc-cosine transform โ
โ โ
โ phi[i] = acos(scaled[i]) โ
โ result in [0, ฯ] โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Build NรN output matrix โ
โ โ
โ for i in 0..N-1: โ
โ for j in 0..N-1: โ
โ result[i,j] = cos(phi[i] โ
โ + phi[j]) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ
โผ
float[N, N] (Result)
03 Windows & Labels
Each training sample is a window of 125 consecutive price bars, split 120 + 5: the first 120 bars become four GAF matrices (Open, High, Low, Close โ four "channels", like RGB in an image), and the last 5 bars become the label.
The label asks: what did price do next? The percent difference between the last analysis close and the average of the 5 future closes is bucketed into one of 13 classes, from DownGreaterThan20Percent through Neutral to UpGreaterThan20Percent, then one-hot encoded as float[13]. Moves beyond ยฑ30% are treated as data errors and filtered.
125 PriceData bars
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ Analysis period (bars 0โ119) โ Feature โ
โ 120 bars โ period โ
โ โ 5 bars โ
โ GAF(Open) โ float[120,120] โ (120โ124) โ
โ GAF(High) โ float[120,120] โ โ
โ GAF(Low) โ float[120,120] โ โ
โ GAF(Close) โ float[120,120] โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโฌโโโโโโ
โ
โโโโโโโโโโโโโโโโโโ
โผ
lastClose = bar[119].Close
avgFuture = avg(bars[120..124].Close)
pctDiff = (avgFuture - lastClose) / lastClose
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Bucket pctDiff into 13 classes โ
โ โ
โ > +30% โ (no label, filtered) โ
โ +20โ30% โ UpGreaterThan20 โ
โ +10โ20% โ Up10To20 โ
โ +5โ10% โ Up5To10 โ
โ +2โ5% โ Up2To5 โ
โ +1โ2% โ Up1To2 โ
โ +0.5โ1% โ Up05To1 โ
โ ยฑ0.5% โ Neutral โ
โ -0.5โ1% โ Down05To1 โ
โ -1โ2% โ Down1To2 โ
โ -2โ5% โ Down2To5 โ
โ -5โ10% โ Down5To10 โ
โ -10โ20% โ Down10To20 โ
โ -20โ30% โ DownGreaterThan20 โ
โ < -30% โ (no label, filtered) โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
ChartFeatureInformation (one bool set)
โ
โผ
OneHot() โ float[13]
e.g. Neutral โ [0,0,0,0,0,0,1,0,0,0,0,0,0]
04 Bulk Generation
A C# ETL task (CreateGafChartFeaturesInDb) drives generation across every market in MongoDB. For each ticker and timeframe it slides the 125-bar window one bar at a time across all available history โ every position becomes one labeled training sample, bulk-inserted as a MongoDB document holding all four matrices and the one-hot label.
- Markets with fewer than 500 bars are skipped โ not enough signal
- Daily, weekly, and monthly passes per market
- Optional parallel execution (Parallel.ForEach, max degree 10)
All price bars for one ticker + TimeLevel
โโโโโฌโโโโฌโโโโฌโโโโฌโโโโฌโโโโฌโ โ โ โฌโโโโ
โ 0 โ 1 โ 2 โ 3 โ...โ124โ โ N โ
โโโโโดโโโโดโโโโดโโโโดโโโโดโโโโดโ โ โ โดโโโโ
i=0 [0 โโโโโโโโโโโโโโโโ 124]
i=1 [1 โโโโโโโโโโโโโโโโ 125]
i=2 [2 โโโโโโโโโโโโโโโโ 126]
...
i=N-125 [N-125 โโโโโโโ N-1]
Each window โ GafFeature โ GafChartFeatureResult
โ
After all windows complete: โ
โผ
List<GafChartFeature>
โ
โผ
MongoPriceDataFeatureDataStorage
.Insert(ticker, timeLevel, list)
โ
โผ
MongoDB: GafChartFeature collection
05 CNN Training
The Python side read the matrices from MongoDB and rendered them to PNG images โ pixel intensity mapped from each cell's value in [-1, +1]. Images were written into a folder tree organized by outcome class, split 70% train / 30% test chronologically, so the test set represents genuinely unseen future data.
A standard Keras/TensorFlow CNN classifier trained on the image tree, with the 13 class folders providing labels automatically. The intended production loop โ current window โ GAF โ image โ CNN โ trade signal above an 80% confidence threshold โ was designed but never built.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ C# โ SharpTechnicals.FileGenerator โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Program.cs
โ
โผ
CreateGafChartFeaturesInDb.Run()
โ
โโ MongoGenericDataStorage<MarketData>
โโ MongoPriceDataStorage
โโ TiingoConnector
โโ MarketUniverse
โ
โผ
GetUpdateableMarkets()
โ
โผ
Filter: Ticker == "SPY"
โ
โผ
ProcessMarket.Process(mrkt)
โ
โโโโบ Daily bars
โโโโบ Weekly bars
โโโโบ Monthly bars
โ
โผ
bars.Count < 500? โโ YES โโโบ (skip)
โ NO
โผ
Slide 125-bar window (step = 1)
โ
โผ
GafFeature(window)
โ
โโโโโโโโโโโดโโโโโโโโโโ
โผ โผ
GAF on Open/High/ ChartFeatureInfo
Low/Close (120 bars) from next 5 bars
โ 4ร float[120,120] โ OneHot float[13]
โ โ
โโโโโโโโโโโฌโโโโโโโโโโ
โผ
GafChartFeature โ MongoDB
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Python โ external โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Read GafChartFeature from MongoDB
โ
โผ
Render float[120,120] โ PNG images
โ
โผ
Split chronologically: 70% TRAIN / 30% TEST
โ
โผ
Folder tree by outcome class
โโโ TRAIN/Up2To5Percent/...
โโโ TEST/Down1To2Percent/...
โ
โผ
CNN training (Keras / TensorFlow)
softmax over 13 outcome classes
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Intended inference (never built) โ
โ window โ GAF โ PNG โ CNN predict โ
โ if P(class) > 0.80 โ trade signal โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Results were lackluster.
Experiment discontinued.
06 Outcome โ Why It Was Shut Down
The full pipeline ran end-to-end: dataset generation, image export, CNN training. The model did not generalize well enough to be useful as a signal, and the experiment was discontinued rather than tuned into the ground. Likely contributing factors:
- Training data was initially limited to SPY only โ limited pattern variety
- 13-class label granularity was probably too fine for the data volume
- GAF matrices encode relative temporal structure but lose absolute price level
- A 5-bar forward average is a noisy label โ one large bar can flip the class
The engineering takeaway: the pipeline architecture โ C# feature generation, MongoDB persistence, Python training โ worked exactly as designed. The hypothesis didn't survive contact with the data, and knowing when to stop is part of the job.