|
| 1 | +// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/ |
| 2 | +// © capissimo |
| 3 | + |
| 4 | +//@version=5 |
| 5 | +indicator('Machine Learning: kNN-based Strategy', 'ML-kNN', true, max_labels_count=300, format=format.price, precision=2, timeframe="", timeframe_gaps=true) |
| 6 | + |
| 7 | +// kNN-based Strategy (FX and Crypto) |
| 8 | +// Description: |
| 9 | +// This strategy uses a classic machine learning algorithm - k Nearest Neighbours (kNN) - |
| 10 | +// to let you find a prediction for the next (tomorrow's, next month's, etc.) market move. |
| 11 | +// Being an unsupervised machine learning algorithm, kNN is one of the most simple learning algorithms. |
| 12 | + |
| 13 | +// To do a prediction of the next market move, the kNN algorithm uses the historic data, |
| 14 | +// collected in 3 arrays - feature1, feature2 and directions, - and finds the k-nearest |
| 15 | +// neighbours of the current indicator(s) values. |
| 16 | + |
| 17 | +// The two dimensional kNN algorithm just has a look on what has happened in the past when |
| 18 | +// the two indicators had a similar level. It then looks at the k nearest neighbours, |
| 19 | +// sees their state and thus classifies the current point. |
| 20 | + |
| 21 | +// The kNN algorithm offers a framework to test all kinds of indicators easily to see if they |
| 22 | +// have got any *predictive value*. One can easily add cog, wpr and others. |
| 23 | +// Note: TradingViews's playback feature helps to see this strategy in action. |
| 24 | +// Warning: Signals ARE repainting. |
| 25 | + |
| 26 | +// Style tags: Trend Following, Trend Analysis |
| 27 | +// Asset class: Equities, Futures, ETFs, Currencies and Commodities |
| 28 | +// Dataset: FX Minutes/Hours+++/Days |
| 29 | + |
| 30 | +//-- Preset Dates |
| 31 | + |
| 32 | +int startdate = timestamp('01 Jan 2000 00:00:00 GMT+10') |
| 33 | +int stopdate = timestamp('31 Dec 2025 23:45:00 GMT+10') |
| 34 | + |
| 35 | +//-- Inputs |
| 36 | + |
| 37 | +StartDate = input.time (startdate, 'Start Date') |
| 38 | +StopDate = input.time (stopdate, 'Stop Date') |
| 39 | +Indicator = input.string('All', 'Indicator', ['RSI','ROC','CCI','Volume','All']) |
| 40 | +ShortWinow = input.int (14, 'Short Period [1..n]', 1) |
| 41 | +LongWindow = input.int (28, 'Long Period [2..n]', 2) |
| 42 | +BaseK = input.int (252, 'Base No. of Neighbours (K) [5..n]', 5) |
| 43 | +Filter = input.bool (false, 'Volatility Filter') |
| 44 | +Bars = input.int (300, 'Bar Threshold [2..5000]', 2, 5000) |
| 45 | + |
| 46 | +//-- Constants |
| 47 | + |
| 48 | +var int BUY = 1 |
| 49 | +var int SELL =-1 |
| 50 | +var int CLEAR = 0 |
| 51 | + |
| 52 | +var int k = math.floor(math.sqrt(BaseK)) // k Value for kNN algo |
| 53 | + |
| 54 | +//-- Variable |
| 55 | + |
| 56 | +// Training data, normalized to the range of [0,...,100] |
| 57 | +var array<float> feature1 = array.new_float(0) // [0,...,100] |
| 58 | +var array<float> feature2 = array.new_float(0) // ... |
| 59 | +var array<int> directions = array.new_int(0) // [-1; +1] |
| 60 | + |
| 61 | +// Result data |
| 62 | +var array<int> predictions = array.new_int(0) |
| 63 | +var float prediction = 0.0 |
| 64 | +var array<int> bars = array.new<int>(1, 0) // array used as a container for inter-bar variables |
| 65 | + |
| 66 | +// Signals |
| 67 | +var int signal = CLEAR |
| 68 | + |
| 69 | +//-- Functions |
| 70 | + |
| 71 | +minimax(float x, int p, float min, float max) => |
| 72 | + float hi = ta.highest(x, p), float lo = ta.lowest(x, p) |
| 73 | + (max - min) * (x - lo)/(hi - lo) + min |
| 74 | + |
| 75 | +cAqua(int g) => g>9?#0080FFff:g>8?#0080FFe5:g>7?#0080FFcc:g>6?#0080FFb2:g>5?#0080FF99:g>4?#0080FF7f:g>3?#0080FF66:g>2?#0080FF4c:g>1?#0080FF33:#00C0FF19 |
| 76 | +cPink(int g) => g>9?#FF0080ff:g>8?#FF0080e5:g>7?#FF0080cc:g>6?#FF0080b2:g>5?#FF008099:g>4?#FF00807f:g>3?#FF008066:g>2?#FF00804c:g>1?#FF008033:#FF008019 |
| 77 | + |
| 78 | +inside_window(float start, float stop) => |
| 79 | + time >= start and time <= stop ? true : false |
| 80 | + |
| 81 | +//-- Logic |
| 82 | + |
| 83 | +bool window = inside_window(StartDate, StopDate) |
| 84 | + |
| 85 | +// 3 pairs of predictor indicators, long and short each |
| 86 | +float rs = ta.rsi(close, LongWindow), float rf = ta.rsi(close, ShortWinow) |
| 87 | +float cs = ta.cci(close, LongWindow), float cf = ta.cci(close, ShortWinow) |
| 88 | +float os = ta.roc(close, LongWindow), float of = ta.roc(close, ShortWinow) |
| 89 | +float vs = minimax(volume, LongWindow, 0, 99), float vf = minimax(volume, ShortWinow, 0, 99) |
| 90 | + |
| 91 | +// TOADD or TOTRYOUT: |
| 92 | +// ta.cmo(close, LongWindow), ta.cmo(close, ShortWinow) |
| 93 | +// ta.mfi(close, LongWindow), ta.mfi(close, ShortWinow) |
| 94 | +// ta.mom(close, LongWindow), ta.mom(close, ShortWinow) |
| 95 | + |
| 96 | +float f1 = switch Indicator |
| 97 | + 'RSI' => rs |
| 98 | + 'CCI' => cs |
| 99 | + 'ROC' => os |
| 100 | + 'Volume' => vs |
| 101 | + => math.avg(rs, cs, os, vs) |
| 102 | + |
| 103 | +float f2 = switch Indicator |
| 104 | + 'RSI' => rf |
| 105 | + 'CCI' => cf |
| 106 | + 'ROC' => of |
| 107 | + 'Volume' => vf |
| 108 | + => math.avg(rf, cf, of, vf) |
| 109 | + |
| 110 | +// Classification data, what happens on the next bar |
| 111 | +int class_label = int(math.sign(close[1] - close[0])) // eq. close[1]<close[0] ? SELL: close[1]>close[0] ? BUY : CLEAR |
| 112 | + |
| 113 | +// Use particular training period |
| 114 | +if window |
| 115 | + // Store everything in arrays. Features represent a square 100 x 100 matrix, |
| 116 | + // whose row-colum intersections represent class labels, showing historic directions |
| 117 | + array.push(feature1, f1) |
| 118 | + array.push(feature2, f2) |
| 119 | + array.push(directions, class_label) |
| 120 | + |
| 121 | +// Ucomment the followng statement (if barstate.islast) and tab everything below |
| 122 | +// between BOBlock and EOBlock marks to see just the recent several signals gradually |
| 123 | +// showing up, rather than all the preceding signals |
| 124 | + |
| 125 | +//if barstate.islast |
| 126 | + |
| 127 | +//==BOBlock |
| 128 | + |
| 129 | +// Core logic of the algorithm |
| 130 | +int size = array.size(directions) |
| 131 | +float maxdist = -999.0 |
| 132 | +// Loop through the training arrays, getting distances and corresponding directions. |
| 133 | +for i=0 to size-1 |
| 134 | + // Calculate the euclidean distance of current point to all historic points, |
| 135 | + // here the metric used might as well be a manhattan distance or any other. |
| 136 | + float d = math.sqrt(math.pow(f1 - array.get(feature1, i), 2) + math.pow(f2 - array.get(feature2, i), 2)) |
| 137 | + |
| 138 | + if d > maxdist |
| 139 | + maxdist := d |
| 140 | + if array.size(predictions) >= k |
| 141 | + array.shift(predictions) |
| 142 | + array.push(predictions, array.get(directions, i)) |
| 143 | + |
| 144 | +//==EOBlock |
| 145 | + |
| 146 | +// Note: in this setup there's no need for distances array (i.e. array.push(distances, d)), |
| 147 | +// but the drawback is that a sudden max value may shadow all the subsequent values. |
| 148 | +// One of the ways to bypass this is to: |
| 149 | +// 1) store d in distances array, |
| 150 | +// 2) calculate newdirs = bubbleSort(distances, directions), and then |
| 151 | +// 3) take a slice with array.slice(newdirs) from the end |
| 152 | + |
| 153 | +// Get the overall prediction of k nearest neighbours |
| 154 | +prediction := array.sum(predictions) |
| 155 | + |
| 156 | +bool filter = Filter ? ta.atr(10) > ta.atr(40) : true // filter out by volatility or ex. ta.atr(1) > ta.atr(10)... |
| 157 | + |
| 158 | +// Now that we got a prediction for the next market move, we need to make use of this prediction and |
| 159 | +// trade it. The returns then will show if everything works as predicted. |
| 160 | +// Over here is a simple long/short interpretation of the prediction, |
| 161 | +// but of course one could also use the quality of the prediction (+5 or +1) in some sort of way, |
| 162 | +// ex. for position sizing. |
| 163 | + |
| 164 | +bool long = prediction > 0 and filter |
| 165 | +bool short = prediction < 0 and filter |
| 166 | +bool clear = not(long and short) |
| 167 | + |
| 168 | +if array.get(bars, 0)==Bars // stop by trade duration |
| 169 | + signal := CLEAR |
| 170 | + array.set(bars, 0, 0) |
| 171 | +else |
| 172 | + array.set(bars, 0, array.get(bars, 0) + 1) |
| 173 | + |
| 174 | +signal := long ? BUY : short ? SELL : clear ? CLEAR : nz(signal[1]) |
| 175 | + |
| 176 | +int changed = ta.change(signal) |
| 177 | +bool startLongTrade = changed and signal==BUY |
| 178 | +bool startShortTrade = changed and signal==SELL |
| 179 | +// bool endLongTrade = changed and signal==SELL |
| 180 | +// bool endShortTrade = changed and signal==BUY |
| 181 | +bool clear_condition = changed and signal==CLEAR //or (changed and signal==SELL) or (changed and signal==BUY) |
| 182 | + |
| 183 | +float maxpos = ta.highest(high, 10) |
| 184 | +float minpos = ta.lowest (low, 10) |
| 185 | + |
| 186 | +//-- Visuals |
| 187 | + |
| 188 | +plotshape(startLongTrade ? minpos : na, 'Buy', shape.labelup, location.belowbar, cAqua(int(prediction*5)), size=size.small) // color intensity correction |
| 189 | +plotshape(startShortTrade ? maxpos : na, 'Sell', shape.labeldown, location.abovebar, cPink(int(-prediction*5)), size=size.small) |
| 190 | +// plot(endLongTrade ? ohlc4 : na, 'StopBuy', cAqua(6), 3, plot.style_cross) |
| 191 | +// plot(endShortTrade ? ohlc4 : na, 'StopSell', cPink(6), 3, plot.style_cross) |
| 192 | +plot(clear_condition ? close : na, 'ClearPos', color.yellow, 4, plot.style_cross) |
| 193 | + |
| 194 | + |
| 195 | +//-- Notification |
| 196 | + |
| 197 | +// if changed and signal==BUY |
| 198 | +// alert('Buy Alert', alert.freq_once_per_bar) // alert.freq_once_per_bar_close |
| 199 | +// if changed and signal==SELL |
| 200 | +// alert('Sell Alert', alert.freq_once_per_bar) |
| 201 | + |
| 202 | +alertcondition(startLongTrade, 'Buy', 'Go long!') |
| 203 | +alertcondition(startShortTrade, 'Sell', 'Go short!') |
| 204 | +//alertcondition(startLongTrade or startShortTrade, 'Alert', 'Deal Time!') |
| 205 | + |
| 206 | +//-------------------- Backtesting (TODO) |
| 207 | + |
| 208 | +// show_cumtr = input.bool (false, 'Show Trade Return?') |
| 209 | +// lot_size = input.float(100.0, 'Lot Size', [0.1,0.2,0.3,0.5,1,2,3,5,10,20,30,50,100,1000,2000,3000,5000,10000]) |
| 210 | + |
| 211 | +// var start_lt = 0. |
| 212 | +// var long_trades = 0. |
| 213 | +// var start_st = 0. |
| 214 | +// var short_trades = 0. |
| 215 | + |
| 216 | +// if startLongTrade |
| 217 | +// start_lt := ohlc4 |
| 218 | +// if endLongTrade |
| 219 | +// long_trades := (open - start_lt) * lot_size |
| 220 | +// if startShortTrade |
| 221 | +// start_st := ohlc4 |
| 222 | +// if endShortTrade |
| 223 | +// short_trades := (start_st - open) * lot_size |
| 224 | + |
| 225 | +// cumreturn = ta.cum(long_trades) + ta.cum(short_trades) |
| 226 | + |
| 227 | +// var label lbl = na |
| 228 | +// if show_cumtr //and barstate.islast |
| 229 | +// lbl := label.new(bar_index+10, close, 'CumReturn: ' + str.tostring(cumreturn, '#.#'), xloc.bar_index, yloc.price, |
| 230 | +// color.new(color.blue, 100), label.style_label_left, color.black, size.small, text.align_left) |
| 231 | +// label.delete(lbl[1]) |
0 commit comments