Facebook
From jason, 1 Month ago, written in Plain Text.
Embed
Download Paste or View Raw
Hits: 199
  1. // This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
  2. // ©jdehorty
  3.  
  4. // @version=5
  5. indicator('Machine Learning: Lorentzian Classification', 'Lorentzian Classification', true, precision=4, max_labels_count=500)
  6.  
  7. import jdehorty/MLExtensions/2 as ml
  8. import jdehorty/KernelFunctions/2 as kernels
  9.  
  10. // ====================
  11. // ==== Background ====
  12. // ====================
  13.  
  14. // When using Machine Learning algorithms like K-Nearest Neighbors, choosing an
  15. // appropriate distance metric is essential. Euclidean Distance is often used as
  16. // the default distance metric, but it may not always be the best choice. This is
  17. // because market data is often significantly impacted by proximity to significant
  18. // world events such as FOMC Meetings and Black Swan events. These major economic
  19. // events can contribute to a warping effect analogous a massive object's
  20. // gravitational warping of Space-Time. In financial markets, this warping effect
  21. // operates on a continuum, which can analogously be referred to as "Price-Time".
  22.  
  23. // To help to better account for this warping effect, Lorentzian Distance can be
  24. // used as an alternative distance metric to Euclidean Distance. The geometry of
  25. // Lorentzian Space can be difficult to visualize at first, and one of the best
  26. // ways to intuitively understand it is through an example involving 2 feature
  27. // dimensions (z=2). For purposes of this example, let's assume these two features
  28. // are Relative Strength Index (RSI) and the Average Directional Index (ADX). In
  29. // reality, the optimal number of features is in the range of 3-8, but for the sake
  30. // of simplicity, we will use only 2 features in this example.
  31.  
  32. // Fundamental Assumptions:
  33. // (1) We can calculate RSI and ADX for a given chart.
  34. // (2) For simplicity, values for RSI and ADX are assumed to adhere to a Gaussian
  35. //     distribution in the range of 0 to 100.
  36. // (3) The most recent RSI and ADX value can be considered the origin of a coordinate
  37. //     system with ADX on the x-axis and RSI on the y-axis.
  38.  
  39. // Distances in Euclidean Space:
  40. // Measuring the Euclidean Distances of historical values with the most recent point
  41. // at the origin will yield a distribution that resembles Figure 1 (below).
  42.  
  43. //                        [RSI]
  44. //                          |                      
  45. //                          |                  
  46. //                          |                
  47. //                      ...:::....              
  48. //                .:.:::••••••:::•::..            
  49. //              .:•:.:•••::::••::••....::.            
  50. //             ....:••••:••••••••::••:...:•.          
  51. //            ...:.::::::•••:::•••:•••::.:•..          
  52. //            ::•:.:•:•••••••:.:•::::::...:..        
  53. //  |--------.:•••..•••••••:••:...:::•:•:..:..----------[ADX]    
  54. //  0        :•:....:•••••::.:::•••::••:.....            
  55. //           ::....:.:••••••••:•••::••::..:.          
  56. //            .:...:••:::••••••••::•••....:          
  57. //              ::....:.....:•::•••:::::..            
  58. //                ..:..::••..::::..:•:..              
  59. //                    .::..:::.....:                
  60. //                          |            
  61. //                          |                  
  62. //                          |
  63. //                          |
  64. //                         _|_ 0        
  65. //                          
  66. //        Figure 1: Neighborhood in Euclidean Space
  67.  
  68. // Distances in Lorentzian Space:
  69. // However, the same set of historical values measured using Lorentzian Distance will
  70. // yield a different distribution that resembles Figure 2 (below).
  71.  
  72. //                        
  73. //                         [RSI]
  74. //  ::..                     |                    ..:::  
  75. //   .....                   |                  ......
  76. //    .••••::.               |               :••••••.
  77. //     .:•••••:.             |            :::••••••.  
  78. //       .•••••:...          |         .::.••••••.    
  79. //         .::•••••::..      |       :..••••••..      
  80. //            .:•••••••::.........::••••••:..        
  81. //              ..::::••••.•••••••.•••••••:.            
  82. //                ...:•••••••.•••••••••::.              
  83. //                  .:..••.••••••.••••..                
  84. //  |---------------.:•••••••••••••••••.---------------[ADX]          
  85. //  0             .:•:•••.••••••.•••••••.                
  86. //              .••••••••••••••••••••••••:.            
  87. //            .:••••••••••::..::.::••••••••:.          
  88. //          .::••••••::.     |       .::•••:::.      
  89. //         .:••••••..        |          :••••••••.    
  90. //       .:••••:...          |           ..•••••••:.  
  91. //     ..:••::..             |              :.•••••••.  
  92. //    .:•....                |               ...::.:••.  
  93. //   ...:..                  |                   :...:••.    
  94. //  :::.                     |                       ..::  
  95. //                          _|_ 0
  96. //
  97. //       Figure 2: Neighborhood in Lorentzian Space
  98.  
  99.  
  100. // Observations:
  101. // (1) In Lorentzian Space, the shortest distance between two points is not
  102. //     necessarily a straight line, but rather, a geodesic curve.
  103. // (2) The warping effect of Lorentzian distance reduces the overall influence  
  104. //     of outliers and noise.
  105. // (3) Lorentzian Distance becomes increasingly different from Euclidean Distance
  106. //     as the number of nearest neighbors used for comparison increases.
  107.  
  108. // ======================
  109. // ==== Custom Types ====
  110. // ======================
  111.  
  112. // This section uses PineScript's new Type syntax to define important data structures
  113. // used throughout the script.
  114.  
  115. type Settings
  116.     float source
  117.     int neighborsCount
  118.     int maxBarsBack
  119.     int featureCount
  120.     int colorCompression
  121.     bool showExits
  122.     bool useDynamicExits
  123.  
  124. type Label
  125.     int long
  126.     int short
  127.     int neutral
  128.  
  129. type FeatureArrays
  130.     array<float> f1
  131.     array<float> f2
  132.     array<float> f3
  133.     array<float> f4
  134.     array<float> f5
  135.  
  136. type FeatureSeries
  137.     float f1
  138.     float f2
  139.     float f3
  140.     float f4
  141.     float f5
  142.  
  143. type MLModel
  144.     int firstBarIndex
  145.     array<int> trainingLabels
  146.     int loopSize
  147.     float lastDistance
  148.     array<float> distancesArray
  149.     array<int> predictionsArray
  150.     int prediction
  151.  
  152. type FilterSettings
  153.     bool useVolatilityFilter
  154.     bool useRegimeFilter
  155.     bool useAdxFilter
  156.     float regimeThreshold
  157.     int adxThreshold
  158.  
  159. type Filter
  160.     bool volatility
  161.     bool regime
  162.     bool adx
  163.  
  164. // ==========================
  165. // ==== Helper Functions ====
  166. // ==========================
  167.  
  168. series_from(feature_string, _close, _high, _low, _hlc3, f_paramA, f_paramB) =>
  169.     switch feature_string
  170.         "RSI" => ml.n_rsi(_close, f_paramA, f_paramB)
  171.         "WT" => ml.n_wt(_hlc3, f_paramA, f_paramB)
  172.         "CCI" => ml.n_cci(_close, f_paramA, f_paramB)
  173.         "ADX" => ml.n_adx(_high, _low, _close, f_paramA)
  174.  
  175. get_lorentzian_distance(int i, int featureCount, FeatureSeries featureSeries, FeatureArrays featureArrays) =>
  176.     switch featureCount
  177.         5 => math.log(1+math.abs(featureSeries.f1 - array.get(featureArrays.f1, i))) +
  178.              math.log(1+math.abs(featureSeries.f2 - array.get(featureArrays.f2, i))) +
  179.              math.log(1+math.abs(featureSeries.f3 - array.get(featureArrays.f3, i))) +
  180.              math.log(1+math.abs(featureSeries.f4 - array.get(featureArrays.f4, i))) +
  181.              math.log(1+math.abs(featureSeries.f5 - array.get(featureArrays.f5, i)))
  182.         4 => math.log(1+math.abs(featureSeries.f1 - array.get(featureArrays.f1, i))) +
  183.              math.log(1+math.abs(featureSeries.f2 - array.get(featureArrays.f2, i))) +
  184.              math.log(1+math.abs(featureSeries.f3 - array.get(featureArrays.f3, i))) +
  185.              math.log(1+math.abs(featureSeries.f4 - array.get(featureArrays.f4, i)))
  186.         3 => math.log(1+math.abs(featureSeries.f1 - array.get(featureArrays.f1, i))) +
  187.              math.log(1+math.abs(featureSeries.f2 - array.get(featureArrays.f2, i))) +
  188.              math.log(1+math.abs(featureSeries.f3 - array.get(featureArrays.f3, i)))
  189.         2 => math.log(1+math.abs(featureSeries.f1 - array.get(featureArrays.f1, i))) +
  190.              math.log(1+math.abs(featureSeries.f2 - array.get(featureArrays.f2, i)))
  191.  
  192. // ================  
  193. // ==== Inputs ====
  194. // ================
  195.  
  196. // Settings Object: General User-Defined Inputs
  197. Settings settings =
  198.  Settings.new(
  199.    input.source(title='Source', defval=close, group="General Settings", tooltip="Source of the input data"),
  200.    input.int(title='Neighbors Count', defval=8, group="General Settings", minval=1, maxval=100, step=1, tooltip="Number of neighbors to consider"),
  201.    input.int(title="Max Bars Back", defval=2000, group="General Settings"),
  202.    input.int(title="Feature Count", defval=5, group="Feature Engineering", minval=2, maxval=5, tooltip="Number of features to use for ML predictions."),
  203.    input.int(title="Color Compression", defval=1, group="General Settings", minval=1, maxval=10, tooltip="Compression factor for adjusting the intensity of the color scale."),
  204.    input.bool(title="Show Default Exits", defval=false, group="General Settings", tooltip="Default exits occur exactly 4 bars after an entry signal. This corresponds to the predefined length of a trade during the model's training process.", inline="exits"),
  205.    input.bool(title="Use Dynamic Exits", defval=false, group="General Settings", tooltip="Dynamic exits attempt to let profits ride by dynamically adjusting the exit threshold based on kernel regression logic.", inline="exits")
  206.  )
  207.    
  208. // Trade Stats Settings
  209. // Note: The trade stats section is NOT intended to be used as a replacement for proper backtesting. It is intended to be used for calibration purposes only.
  210. showTradeStats = input.bool(true, 'Show Trade Stats', tooltip='Displays the trade stats for a given configuration. Useful for optimizing the settings in the Feature Engineering section. This should NOT replace backtesting and should be used for calibration purposes only. Early Signal Flips represent instances where the model changes signals before 4 bars elapses; high values can indicate choppy (ranging) market conditions.', group="General Settings")
  211. useWorstCase = input.bool(false, "Use Worst Case Estimates", tooltip="Whether to use the worst case scenario for backtesting. This option can be useful for creating a conservative estimate that is based on close prices only, thus avoiding the effects of intrabar repainting. This option assumes that the user does not enter when the signal first appears and instead waits for the bar to close as confirmation. On larger timeframes, this can mean entering after a large move has already occurred. Leaving this option disabled is generally better for those that use this indicator as a source of confluence and prefer estimates that demonstrate discretionary mid-bar entries. Leaving this option enabled may be more consistent with traditional backtesting results.", group="General Settings")
  212.  
  213. // Settings object for user-defined settings
  214. FilterSettings filterSettings =
  215.  FilterSettings.new(
  216.    input.bool(title="Use Volatility Filter", defval=true, tooltip="Whether to use the volatility filter.", group="Filters"),
  217.    input.bool(title="Use Regime Filter", defval=true, group="Filters", inline="regime"),
  218.    input.bool(title="Use ADX Filter", defval=false, group="Filters", inline="adx"),
  219.    input.float(title="Threshold", defval=-0.1, minval=-10, maxval=10, step=0.1, tooltip="Whether to use the trend detection filter. Threshold for detecting Trending/Ranging markets.", group="Filters", inline="regime"),
  220.    input.int(title="Threshold", defval=20, minval=0, maxval=100, step=1, tooltip="Whether to use the ADX filter. Threshold for detecting Trending/Ranging markets.", group="Filters", inline="adx")
  221.  )
  222.  
  223. // Filter object for filtering the ML predictions
  224. Filter filter =
  225.  Filter.new(
  226.    ml.filter_volatility(1, 10, filterSettings.useVolatilityFilter),
  227.    ml.regime_filter(ohlc4, filterSettings.regimeThreshold, filterSettings.useRegimeFilter),
  228.    ml.filter_adx(settings.source, 14, filterSettings.adxThreshold, filterSettings.useAdxFilter)
  229.   )
  230.  
  231. // Feature Variables: User-Defined Inputs for calculating Feature Series.
  232. f1_string = input.string(title="Feature 1", options=["RSI", "WT", "CCI", "ADX"], defval="RSI", inline = "01", tooltip="The first feature to use for ML predictions.", group="Feature Engineering")
  233. f1_paramA = input.int(title="Parameter A", tooltip="The primary parameter of feature 1.", defval=14, inline = "02", group="Feature Engineering")
  234. f1_paramB = input.int(title="Parameter B", tooltip="The secondary parameter of feature 2 (if applicable).", defval=1, inline = "02", group="Feature Engineering")
  235. f2_string = input.string(title="Feature 2", options=["RSI", "WT", "CCI", "ADX"], defval="WT", inline = "03", tooltip="The second feature to use for ML predictions.", group="Feature Engineering")
  236. f2_paramA = input.int(title="Parameter A", tooltip="The primary parameter of feature 2.", defval=10, inline = "04", group="Feature Engineering")
  237. f2_paramB = input.int(title="Parameter B", tooltip="The secondary parameter of feature 2 (if applicable).", defval=11, inline = "04", group="Feature Engineering")
  238. f3_string = input.string(title="Feature 3", options=["RSI", "WT", "CCI", "ADX"], defval="CCI", inline = "05", tooltip="The third feature to use for ML predictions.", group="Feature Engineering")
  239. f3_paramA = input.int(title="Parameter A", tooltip="The primary parameter of feature 3.", defval=20, inline = "06", group="Feature Engineering")
  240. f3_paramB = input.int(title="Parameter B", tooltip="The secondary parameter of feature 3 (if applicable).", defval=1, inline = "06", group="Feature Engineering")
  241. f4_string = input.string(title="Feature 4", options=["RSI", "WT", "CCI", "ADX"], defval="ADX", inline = "07", tooltip="The fourth feature to use for ML predictions.", group="Feature Engineering")
  242. f4_paramA = input.int(title="Parameter A", tooltip="The primary parameter of feature 4.", defval=20, inline = "08", group="Feature Engineering")
  243. f4_paramB = input.int(title="Parameter B", tooltip="The secondary parameter of feature 4 (if applicable).", defval=2, inline = "08", group="Feature Engineering")
  244. f5_string = input.string(title="Feature 5", options=["RSI", "WT", "CCI", "ADX"], defval="RSI", inline = "09", tooltip="The fifth feature to use for ML predictions.", group="Feature Engineering")
  245. f5_paramA = input.int(title="Parameter A", tooltip="The primary parameter of feature 5.", defval=9, inline = "10", group="Feature Engineering")
  246. f5_paramB = input.int(title="Parameter B", tooltip="The secondary parameter of feature 5 (if applicable).", defval=1, inline = "10", group="Feature Engineering")
  247.  
  248. // FeatureSeries Object: Calculated Feature Series based on Feature Variables
  249. featureSeries =
  250.  FeatureSeries.new(
  251.    series_from(f1_string, close, high, low, hlc3, f1_paramA, f1_paramB), // f1
  252.    series_from(f2_string, close, high, low, hlc3, f2_paramA, f2_paramB), // f2
  253.    series_from(f3_string, close, high, low, hlc3, f3_paramA, f3_paramB), // f3
  254.    series_from(f4_string, close, high, low, hlc3, f4_paramA, f4_paramB), // f4
  255.    series_from(f5_string, close, high, low, hlc3, f5_paramA, f5_paramB)  // f5
  256.  )
  257.  
  258. // FeatureArrays Variables: Storage of Feature Series as Feature Arrays Optimized for ML
  259. // Note: These arrays cannot be dynamically created within the FeatureArrays Object Initialization and thus must be set-up in advance.
  260. var f1Array = array.new_float()
  261. var f2Array = array.new_float()
  262. var f3Array = array.new_float()
  263. var f4Array = array.new_float()
  264. var f5Array = array.new_float()
  265. array.push(f1Array, featureSeries.f1)
  266. array.push(f2Array, featureSeries.f2)
  267. array.push(f3Array, featureSeries.f3)
  268. array.push(f4Array, featureSeries.f4)
  269. array.push(f5Array, featureSeries.f5)
  270.  
  271. // FeatureArrays Object: Storage of the calculated FeatureArrays into a single object
  272. featureArrays =
  273.  FeatureArrays.new(
  274.   f1Array, // f1
  275.   f2Array, // f2
  276.   f3Array, // f3
  277.   f4Array, // f4
  278.   f5Array  // f5
  279.  )
  280.  
  281. // Label Object: Used for classifying historical data as training data for the ML Model
  282. Label direction =
  283.  Label.new(
  284.    long=1,
  285.    short=-1,
  286.    neutral=0
  287.   )
  288.  
  289. // Derived from General Settings
  290. maxBarsBackIndex = last_bar_index >= settings.maxBarsBack ? last_bar_index - settings.maxBarsBack : 0
  291.  
  292. // EMA Settings
  293. useEmaFilter = input.bool(title="Use EMA Filter", defval=false, group="Filters", inline="ema")
  294. emaPeriod = input.int(title="Period", defval=200, minval=1, step=1, group="Filters", inline="ema", tooltip="The period of the EMA used for the EMA Filter.")
  295. isEmaUptrend = useEmaFilter ? close > ta.ema(close, emaPeriod) : true
  296. isEmaDowntrend = useEmaFilter ? close < ta.ema(close, emaPeriod) : true
  297. useSmaFilter = input.bool(title="Use SMA Filter", defval=false, group="Filters", inline="sma")
  298. smaPeriod = input.int(title="Period", defval=200, minval=1, step=1, group="Filters", inline="sma", tooltip="The period of the SMA used for the SMA Filter.")
  299. isSmaUptrend = useSmaFilter ? close > ta.sma(close, smaPeriod) : true
  300. isSmaDowntrend = useSmaFilter ? close < ta.sma(close, smaPeriod) : true
  301.  
  302. // Nadaraya-Watson Kernel Regression Settings
  303. useKernelFilter = input.bool(true, "Trade with Kernel", group="Kernel Settings", inline="kernel")
  304. showKernelEstimate = input.bool(true, "Show Kernel Estimate", group="Kernel Settings", inline="kernel")
  305. useKernelSmoothing = input.bool(false, "Enhance Kernel Smoothing", tooltip="Uses a crossover based mechanism to smoothen kernel color changes. This often results in less color transitions overall and may result in more ML entry signals being generated.", inline='1', group='Kernel Settings')
  306. h = input.int(8, 'Lookback Window', minval=3, tooltip='The number of bars used for the estimation. This is a sliding value that represents the most recent historical bars. Recommended range: 3-50', group="Kernel Settings", inline="kernel")
  307. r = input.float(8., 'Relative Weighting', step=0.25, tooltip='Relative weighting of time frames. As this value approaches zero, the longer time frames will exert more influence on the estimation. As this value approaches infinity, the behavior of the Rational Quadratic Kernel will become identical to the Gaussian kernel. Recommended range: 0.25-25', group="Kernel Settings", inline="kernel")
  308. x = input.int(25, "Regression Level", tooltip='Bar index on which to start regression. Controls how tightly fit the kernel estimate is to the data. Smaller values are a tighter fit. Larger values are a looser fit. Recommended range: 2-25', group="Kernel Settings", inline="kernel")
  309. lag = input.int(2, "Lag", tooltip="Lag for crossover detection. Lower values result in earlier crossovers. Recommended range: 1-2", inline='1', group='Kernel Settings')
  310.  
  311. // Display Settings
  312. showBarColors = input.bool(true, "Show Bar Colors", tooltip="Whether to show the bar colors.", group="Display Settings")
  313.  showBarPredicti = true, title = "Show Bar Prediction Values", tooltip = "Will show the ML model's evaluation of each bar as an integer.", group="Display Settings")
  314. useAtrOffset = input.bool(defval = false, title = "Use ATR Offset", tooltip = "Will use the ATR offset instead of the bar prediction offset.", group="Display Settings")
  315.  barPredicti "Bar Prediction Offset", minval=0, tooltip="The offset of the bar predictions as a percentage from the bar high or close.", group="Display Settings")
  316.  
  317. // =================================
  318.  // ==== Next Bar Classificati
  319. // =================================
  320.  
  321. // This model specializes specifically in predicting the direction of price action over the course of the next 4 bars.
  322. // To avoid complications with the ML model, this value is hardcoded to 4 bars but support for other training lengths may be added in the future.
  323. src = settings.source
  324. y_train_series = src[4] < src[0] ? direction.short : src[4] > src[0] ? direction.long : direction.neutral
  325. var y_train_array = array.new_int(0)
  326.  
  327. // Variables used for ML Logic
  328. var predictions = array.new_float(0)
  329. var prediction = 0.
  330. var signal = direction.neutral
  331. var distances = array.new_float(0)
  332.  
  333. array.push(y_train_array, y_train_series)
  334.  
  335. // =========================
  336. // ====  Core ML Logic  ====
  337. // =========================
  338.  
  339. // Approximate Nearest Neighbors Search with Lorentzian Distance:
  340. // A novel variation of the Nearest Neighbors (NN) search algorithm that ensures a chronologically uniform distribution of neighbors.
  341.  
  342. // In a traditional KNN-based approach, we would iterate through the entire dataset and calculate the distance between the current bar
  343. // and every other bar in the dataset and then sort the distances in ascending order. We would then take the first k bars and use their
  344. // labels to determine the label of the current bar.
  345.  
  346. // There are several problems with this traditional KNN approach in the context of real-time calculations involving time series data:
  347. // - It is computationally expensive to iterate through the entire dataset and calculate the distance between every historical bar and
  348. //   the current bar.
  349. // - Market time series data is often non-stationary, meaning that the statistical properties of the data change slightly over time.
  350. // - It is possible that the nearest neighbors are not the most informative ones, and the KNN algorithm may return poor results if the
  351. //   nearest neighbors are not representative of the majority of the data.
  352.  
  353. // Previously, the user @capissimo attempted to address some of these issues in several of his PineScript-based KNN implementations by:
  354. // - Using a modified KNN algorithm based on consecutive furthest neighbors to find a set of approximate "nearest" neighbors.
  355. // - Using a sliding window approach to only calculate the distance between the current bar and the most recent n bars in the dataset.
  356.  
  357. // Of these two approaches, the latter is inherently limited by the fact that it only considers the most recent bars in the overall dataset.
  358.  
  359. // The former approach has more potential to leverage historical price action, but is limited by:
  360. // - The possibility of a sudden "max" value throwing off the estimation
  361. // - The possibility of selecting a set of approximate neighbors that are not representative of the majority of the data by oversampling
  362. //   values that are not chronologically distinct enough from one another
  363. // - The possibility of selecting too many "far" neighbors, which may result in a poor estimation of price action
  364.  
  365. // To address these issues, a novel Approximate Nearest Neighbors (ANN) algorithm is used in this indicator.
  366.  
  367. // In the below ANN algorithm:
  368. // 1. The algorithm iterates through the dataset in chronological order, using the modulo operator to only perform calculations every 4 bars.
  369. //    This serves the dual purpose of reducing the computational overhead of the algorithm and ensuring a minimum chronological spacing
  370. //    between the neighbors of at least 4 bars.
  371. // 2. A list of the k-similar neighbors is simultaneously maintained in both a predictions array and corresponding distances array.
  372. // 3. When the size of the predictions array exceeds the desired number of nearest neighbors specified in settings.neighborsCount,
  373. //    the algorithm removes the first neighbor from the predictions array and the corresponding distance array.
  374. // 4. The lastDistance variable is overriden to be a distance in the lower 25% of the array. This step helps to boost overall accuracy
  375. //    by ensuring subsequent newly added distance values increase at a slower rate.
  376. // 5. Lorentzian distance is used as a distance metric in order to minimize the effect of outliers and take into account the warping of
  377. //    "price-time" due to proximity to significant economic events.
  378.  
  379. lastDistance = -1.0
  380. size = math.min(settings.maxBarsBack-1, array.size(y_train_array)-1)
  381. sizeLoop = math.min(settings.maxBarsBack-1, size)
  382.  
  383. if bar_index >= maxBarsBackIndex //{
  384.     for i = 0 to sizeLoop //{
  385.         d = get_lorentzian_distance(i, settings.featureCount, featureSeries, featureArrays)
  386.         if d >= lastDistance and i%4 //{
  387.             lastDistance := d            
  388.             array.push(distances, d)
  389.             array.push(predictions, math.round(array.get(y_train_array, i)))
  390.             if array.size(predictions) > settings.neighborsCount //{
  391.                 lastDistance := array.get(distances, math.round(settings.neighborsCount*3/4))
  392.                 array.shift(distances)
  393.                 array.shift(predictions)
  394.             //}
  395.         //}
  396.     //}
  397.     prediction := array.sum(predictions)
  398. //}
  399.  
  400. // ============================
  401. // ==== Prediction Filters ====
  402. // ============================
  403.  
  404. // User Defined Filters: Used for adjusting the frequency of the ML Model's predictions
  405. filter_all = filter.volatility and filter.regime and filter.adx
  406.  
  407. // Filtered Signal: The model's prediction of future price movement direction with user-defined filters applied
  408. signal := prediction > 0 and filter_all ? direction.long : prediction < 0 and filter_all ? direction.short : nz(signal[1])
  409.  
  410. // Bar-Count Filters: Represents strict filters based on a pre-defined holding period of 4 bars
  411. var int barsHeld = 0
  412. barsHeld := ta.change(signal) ? 0 : barsHeld + 1
  413. isHeldFourBars = barsHeld == 4
  414. isHeldLessThanFourBars = 0 < barsHeld and barsHeld < 4
  415.  
  416. // Fractal Filters: Derived from relative appearances of signals in a given time series fractal/segment with a default length of 4 bars
  417. isDifferentSignalType = ta.change(signal)
  418. isEarlySignalFlip = ta.change(signal) and (ta.change(signal[1]) or ta.change(signal[2]) or ta.change(signal[3]))
  419. isBuySignal = signal == direction.long and isEmaUptrend and isSmaUptrend
  420. isSellSignal = signal == direction.short and isEmaDowntrend and isSmaDowntrend
  421. isLastSignalBuy = signal[4] == direction.long and isEmaUptrend[4] and isSmaUptrend[4]
  422. isLastSignalSell = signal[4] == direction.short and isEmaDowntrend[4] and isSmaDowntrend[4]
  423. isNewBuySignal = isBuySignal and isDifferentSignalType
  424. isNewSellSignal = isSellSignal and isDifferentSignalType
  425.  
  426. // Kernel Regression Filters: Filters based on Nadaraya-Watson Kernel Regression using the Rational Quadratic Kernel
  427. // For more information on this technique refer to my other open source indicator located here:
  428. // https://www.tradingview.com/script/AWNvbPRM-Nadaraya-Watson-Rational-Quadratic-Kernel-Non-Repainting/
  429. c_green = color.new(#009988, 20)
  430. c_red = color.new(#CC3311, 20)
  431. transparent = color.new(#000000, 100)
  432. yhat1 = kernels.rationalQuadratic(settings.source, h, r, x)
  433. yhat2 = kernels.gaussian(settings.source, h-lag, x)
  434. kernelEstimate = yhat1
  435. // Kernel Rates of Change
  436. bool wasBearishRate = yhat1[2] > yhat1[1]
  437. bool wasBullishRate = yhat1[2] < yhat1[1]
  438. bool isBearishRate = yhat1[1] > yhat1
  439. bool isBullishRate = yhat1[1] < yhat1
  440. isBearishChange = isBearishRate and wasBullishRate
  441. isBullishChange = isBullishRate and wasBearishRate
  442. // Kernel Crossovers
  443. bool isBullishCrossAlert = ta.crossover(yhat2, yhat1)
  444. bool isBearishCrossAlert = ta.crossunder(yhat2, yhat1)
  445. bool isBullishSmooth = yhat2 >= yhat1
  446. bool isBearishSmooth = yhat2 <= yhat1
  447. // Kernel Colors
  448. color colorByCross = isBullishSmooth ? c_green : c_red
  449. color colorByRate = isBullishRate ? c_green : c_red
  450. color plotColor = showKernelEstimate ? (useKernelSmoothing ? colorByCross : colorByRate) : transparent
  451. plot(kernelEstimate, color=plotColor, linewidth=2, title="Kernel Regression Estimate")
  452. // Alert Variables
  453. bool alertBullish = useKernelSmoothing ? isBullishCrossAlert : isBullishChange
  454. bool alertBearish = useKernelSmoothing ? isBearishCrossAlert : isBearishChange
  455. // Bullish and Bearish Filters based on Kernel
  456. isBullish = useKernelFilter ? (useKernelSmoothing ? isBullishSmooth : isBullishRate) : true
  457. isBearish = useKernelFilter ? (useKernelSmoothing ? isBearishSmooth : isBearishRate) : true
  458.  
  459. // ===========================
  460. // ==== Entries and Exits ====
  461. // ===========================
  462.  
  463. // Entry Conditions: Booleans for ML Model Position Entries
  464.  startL and isBullish and isEmaUptrend and isSmaUptrend
  465. startShortTrade = isNewSellSignal and isBearish and isEmaDowntrend and isSmaDowntrend
  466.  
  467. // Dynamic Exit Conditions: Booleans for ML Model Position Exits based on Fractal Filters and Kernel Regression Filters
  468. lastSignalWasBullish = ta.barssince(startLongTrade) < ta.barssince(startShortTrade)
  469. lastSignalWasBearish = ta.barssince(startShortTrade) < ta.barssince(startLongTrade)
  470. barsSinceRedEntry = ta.barssince(startShortTrade)
  471. barsSinceRedExit = ta.barssince(alertBullish)
  472. barsSinceGreenEntry = ta.barssince(startLongTrade)
  473. barsSinceGreenExit = ta.barssince(alertBearish)
  474. isValidShortExit = barsSinceRedExit > barsSinceRedEntry
  475. isValidLongExit = barsSinceGreenExit > barsSinceGreenEntry
  476. endLongTradeDynamic = (isBearishChange and isValidLongExit[1])
  477. endShortTradeDynamic = (isBullishChange and isValidShortExit[1])
  478.  
  479. // Fixed Exit Conditions: Booleans for ML Model Position Exits based on a Bar-Count Filters
  480. endLongTradeStrict = ((isHeldFourBars and isLastSignalBuy) or (isHeldLessThanFourBars and isNewSellSignal and isLastSignalBuy)) and startLongTrade[4]
  481. endShortTradeStrict = ((isHeldFourBars and isLastSignalSell) or (isHeldLessThanFourBars and isNewBuySignal and isLastSignalSell)) and startShortTrade[4]
  482. isDynamicExitValid = not useEmaFilter and not useSmaFilter and not useKernelSmoothing
  483. endLongTrade = settings.useDynamicExits and isDynamicExitValid ? endLongTradeDynamic : endLongTradeStrict
  484. endShortTrade = settings.useDynamicExits and isDynamicExitValid ? endShortTradeDynamic : endShortTradeStrict
  485.  
  486. // =========================
  487. // ==== Plotting Labels ====
  488. // =========================
  489.  
  490. // Note: These will not repaint once the most recent bar has fully closed. By default, signals appear over the last closed bar; to override this behavior set offset=0.
  491. plotshape(startLongTrade ? low : na, 'Buy', shape.labelup, location.belowbar, color=ml.color_green(prediction), size=size.small, offset=0)
  492. plotshape(startShortTrade ? high : na, 'Sell', shape.labeldown, location.abovebar, ml.color_red(-prediction), size=size.small, offset=0)
  493. plotshape(endLongTrade and settings.showExits ? high : na, 'StopBuy', shape.xcross, location.absolute, color=#3AFF17, size=size.tiny, offset=0)
  494. plotshape(endShortTrade and settings.showExits ? low : na, 'StopSell', shape.xcross, location.absolute, color=#FD1707, size=size.tiny, offset=0)
  495.  
  496. // ================
  497. // ==== Alerts ====
  498. // ================
  499.  
  500. // Separate Alerts for Entries and Exits
  501. alertcondition(startLongTrade, title='Open Long ▲', message='LDC Open Long ▲ | {{ticker}}@{{close}} | ({{interval}})')
  502. alertcondition(endLongTrade, title='Close Long ▲', message='LDC Close Long ▲ | {{ticker}}@{{close}} | ({{interval}})')
  503. alertcondition(startShortTrade, title='Open Short ▼', message='LDC Open Short  | {{ticker}}@{{close}} | ({{interval}})')
  504. alertcondition(endShortTrade, title='Close Short ▼', message='LDC Close Short ▼ | {{ticker}}@{{close}} | ({{interval}})')
  505.  
  506. // Combined Alerts for Entries and Exits
  507. alertcondition(startShortTrade or startLongTrade, title='Open Position ▲▼', message='LDC Open Position ▲▼ | {{ticker}}@{{close}} | ({{interval}})')
  508. alertcondition(endShortTrade or endLongTrade, title='Close Position ▲▼', message='LDC Close Position  ▲▼ | {{ticker}}@[{{close}}] | ({{interval}})')
  509.  
  510. // Kernel Estimate Alerts
  511. alertcondition(condition=alertBullish, title='Kernel Bullish Color Change', message='LDC Kernel Bullish ▲ | {{ticker}}@{{close}} | ({{interval}})')
  512. alertcondition(condition=alertBearish, title='Kernel Bearish Color Change', message='LDC Kernel Bearish ▼ | {{ticker}}@{{close}} | ({{interval}})')
  513.  
  514. // =========================
  515. // ==== Display Signals ====
  516. // =========================
  517.  
  518. atrSpaced = useAtrOffset ? ta.atr(1) : na
  519. compressionFactor = settings.neighborsCount / settings.colorCompression
  520. c_pred = prediction > 0 ? color.from_gradient(prediction, 0, compressionFactor, #787b86, #009988) : prediction <= 0 ? color.from_gradient(prediction, -compressionFactor, 0, #CC3311, #787b86) : na
  521. c_label = showBarPredictions ? c_pred : na
  522. c_bars = showBarColors ? color.new(c_pred, 50) : na
  523. x_val = bar_index
  524. y_val = useAtrOffset ? prediction > 0 ? high + atrSpaced: low - atrSpaced : prediction > 0 ? high + hl2*barPredictionsOffset/20 : low - hl2*barPredictionsOffset/30
  525. label.new(x_val, y_val, str.tostring(prediction), xloc.bar_index, yloc.price, color.new(color.white, 100), label.style_label_up, c_label, size.normal, text.align_left)
  526. barcolor(showBarColors ? color.new(c_pred, 50) : na)
  527.  
  528. // =====================
  529. // ==== Backtesting ====
  530. // =====================
  531.  
  532. // The following can be used to stream signals to a backtest adapter
  533. backTestStream = switch
  534.     startLongTrade => 1
  535.     endLongTrade => 2
  536.     startShortTrade => -1
  537.     endShortTrade => -2
  538. plot(backTestStream, "Backtest Stream", display=display.none)
  539.  
  540. // The following can be used to display real-time trade stats. This can be a useful mechanism for obtaining real-time feedback during Feature Engineering. This does NOT replace the need to properly backtest.
  541. // Note: In this context, a "Stop-Loss" is defined instances where the ML Signal prematurely flips directions before an exit signal can be generated.
  542. [totalWins, totalLosses, totalEarlySignalFlips, totalTrades, tradeStatsHeader, winLossRatio, winRate] = ml.backtest(high, low, open, startLongTrade, endLongTrade, startShortTrade, endShortTrade, isEarlySignalFlip, maxBarsBackIndex, bar_index, settings.source, useWorstCase)
  543.  
  544. init_table() =>
  545.     c_transparent = color.new(color.black, 100)
  546.     table.new(position.top_right, columns=2, rows=7, frame_color=color.new(color.black, 100), frame_width=1, border_width=1, border_color=c_transparent)
  547.  
  548. update_table(tbl, tradeStatsHeader, totalTrades, totalWins, totalLosses, winLossRatio, winRate, stopLosses) =>
  549.     c_transparent = color.new(color.black, 100)
  550.     table.cell(tbl, 0, 0, tradeStatsHeader, text_halign=text.align_center, text_color=color.gray, text_size=size.normal)
  551.     table.cell(tbl, 0, 1, 'Winrate', text_halign=text.align_center, bgcolor=c_transparent, text_color=color.gray, text_size=size.normal)
  552.     table.cell(tbl, 1, 1, str.tostring(totalWins / totalTrades, '#.#%'), text_halign=text.align_center, bgcolor=c_transparent, text_color=color.gray, text_size=size.normal)
  553.     table.cell(tbl, 0, 2, 'Trades', text_halign=text.align_center, bgcolor=c_transparent, text_color=color.gray, text_size=size.normal)
  554.     table.cell(tbl, 1, 2, str.tostring(totalTrades, '#') + ' (' + str.tostring(totalWins, '#') + '|' + str.tostring(totalLosses, '#') + ')', text_halign=text.align_center, bgcolor=c_transparent, text_color=color.gray, text_size=size.normal)
  555.     table.cell(tbl, 0, 5, 'WL Ratio', text_halign=text.align_center, bgcolor=c_transparent, text_color=color.gray, text_size=size.normal)
  556.     table.cell(tbl, 1, 5, str.tostring(totalWins / totalLosses, '0.00'), text_halign=text.align_center, bgcolor=c_transparent, text_color=color.gray, text_size=size.normal)
  557.     table.cell(tbl, 0, 6, 'Early Signal Flips', text_halign=text.align_center, bgcolor=c_transparent, text_color=color.gray, text_size=size.normal)
  558.     table.cell(tbl, 1, 6, str.tostring(totalEarlySignalFlips, '#'), text_halign=text.align_center, bgcolor=c_transparent, text_color=color.gray, text_size=size.normal)
  559.  
  560. if showTradeStats
  561.     var tbl = ml.init_table()
  562.     if barstate.islast
  563.         update_table(tbl, tradeStatsHeader, totalTrades, totalWins, totalLosses, winLossRatio, winRate, totalEarlySignalFlips)