ПрограмиСкриптиТехнологіяУроки та прикладиФриланс

переробити цей скрипт з мови Pine Script на мову Python

Цей Pine Script — це повна стратегія з машинним навчанням, фільтрами, управлінням ризиком і капіталом, понад 1200 рядків коду. Перенести все це “в лоб” у Python неможливо одним кроком — потрібно розділити його на модулі:

  1. Data module — завантаження історичних даних (через yfinance, ccxt, pandas тощо).
  2. Feature engineering — реалізація показників RSI, WT, CCI, ADX.
  3. Lorentzian distance ML core — логіка “сусідів” для прогнозу напрямку.
  4. Filters — EMA/SMA/Supertrend/ADX/Regime.
  5. Position sizing + money management — розрахунок ризику, SL/TP, breakeven.
  6. Backtesting/Execution — використання backtesting.py або vectorbt для торгової логіки.
  7. Visualization — matplotlib або plotly.

Щоб ми могли почати коректну конвертацію, уточни, будь ласка:
Яку частину скрипта ти хочеш конвертувати в першу чергу?

  • Весь скрипт (повна стратегія для backtesting в Python)
  • Тільки частину з Lorentzian Machine Learning класифікатором
  • Тільки частину з управлінням позицією / ризиком
  • Тільки фільтри та сигнали входу/виходу

Чудово — роблю повну конвертацію стратегії в робочий Python-скрипт. Нижче — повний самодостатній Python-скрипт, який реалізує:

  • розрахунок індикаторів (RSI, CCI, ADX, WaveTrend (WT), EMA, SMA, ATR, SuperTrend),
  • функції Nadaraya–Watson з Gaussian та Rational Quadratic kernels для kernel-estimate,
  • Lorentzian-distance «k-nearest neighbors» (реалізація як в Pine — сума log(1+abs(diff)) для обраної кількості фіч),
  • логіку формування міток (next 4 bars → long/short/neutral),
  • застосування фільтрів (volatility, regime, ADX, EMA/SMA),
  • позиційний менеджмент: розрахунок stop loss (ATR або swing high/low), take profit, breakeven, комісія, ризик у %,
  • простий бектестер (tick-by-bar loop) з відкриттям/закриттям позицій згідно сигналів і правилам стратегії,
  • вивід таблиці зі списком угод та базових метрик.

Зверни увагу: це функціональна конвертація — я намагався зберегти логіку Pine якнайточніше, але деякі дрібні відмінності (інтерполяції, edge-cases, exact index shifts) можуть бути — робоча база для подальшої калібрування.

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
//
// Please, visit the main script that strategy is based in 🙂 Machine Learning: Lorentzian Classification/<<<< ==============================================
// Indicator developed by ©jdehorty
// Strategy devoleped by ©StrategiesForEVeryone

// @version=5
strategy(‘Machine Learning: Lorentzian Classification’, ‘Lorentzian Classification Strategy’, true, precision=2, initial_capital = 100000, process_orders_on_close = true, calc_on_every_tick = true, commission_value = 0.03)

import jdehorty/MLExtensions/2 as ml
import jdehorty/KernelFunctions/2 as kernels

initial_message = input.bool(defval = true, title = “Show initial message”, group = “Initial message”)
if barstate.islast and initial_message
label.new(bar_index, 0, “Please, read the entire post ” + “\nfor a better understanding.” + “\nDo not forget to visit the original” + “\nindicator from ©jdehorty and leave your boost!”, xloc = xloc.bar_index, yloc = yloc.abovebar, textcolor = color.white, color = color.rgb(76, 175, 255, 4))

// ======================
// ==== Custom Types ====
// ======================

// This section uses PineScript’s new Type syntax to define important data structures
// used throughout the script.

type Settings
float source
int neighborsCount
int maxBarsBack
int featureCount
int colorCompression
bool showExits
bool useDynamicExits

type Label
int long
int short
int neutral

type FeatureArrays
array f1
array f2
array f3
array f4
array f5

type FeatureSeries
float f1
float f2
float f3
float f4
float f5

type MLModel
int firstBarIndex
array trainingLabels
int loopSize
float lastDistance
array distancesArray
array predictionsArray
int prediction

type FilterSettings
bool useVolatilityFilter
bool useRegimeFilter
bool useAdxFilter
float regimeThreshold
int adxThreshold

type Filter
bool volatility
bool regime
bool adx

// ==========================
// ==== Helper Functions ====
// ==========================

series_from(feature_string, _close, _high, _low, _hlc3, f_paramA, f_paramB) =>
switch feature_string
“RSI” => ml.n_rsi(_close, f_paramA, f_paramB)
“WT” => ml.n_wt(_hlc3, f_paramA, f_paramB)
“CCI” => ml.n_cci(_close, f_paramA, f_paramB)
“ADX” => ml.n_adx(_high, _low, _close, f_paramA)

get_lorentzian_distance(int i, int featureCount, FeatureSeries featureSeries, FeatureArrays featureArrays) =>
switch featureCount
5 => math.log(1+math.abs(featureSeries.f1 – array.get(featureArrays.f1, i))) +
math.log(1+math.abs(featureSeries.f2 – array.get(featureArrays.f2, i))) +
math.log(1+math.abs(featureSeries.f3 – array.get(featureArrays.f3, i))) +
math.log(1+math.abs(featureSeries.f4 – array.get(featureArrays.f4, i))) +
math.log(1+math.abs(featureSeries.f5 – array.get(featureArrays.f5, i)))
4 => math.log(1+math.abs(featureSeries.f1 – array.get(featureArrays.f1, i))) +
math.log(1+math.abs(featureSeries.f2 – array.get(featureArrays.f2, i))) +
math.log(1+math.abs(featureSeries.f3 – array.get(featureArrays.f3, i))) +
math.log(1+math.abs(featureSeries.f4 – array.get(featureArrays.f4, i)))
3 => math.log(1+math.abs(featureSeries.f1 – array.get(featureArrays.f1, i))) +
math.log(1+math.abs(featureSeries.f2 – array.get(featureArrays.f2, i))) +
math.log(1+math.abs(featureSeries.f3 – array.get(featureArrays.f3, i)))
2 => math.log(1+math.abs(featureSeries.f1 – array.get(featureArrays.f1, i))) +
math.log(1+math.abs(featureSeries.f2 – array.get(featureArrays.f2, i)))

// ================
// ==== Inputs ====
// ================

// Settings Object: General User-Defined Inputs
Settings settings =
Settings.new(
input.source(title=’Source’, defval=close, group=”General Settings”, tooltip=”Source of the input data”),
input.int(title=’Neighbors Count’, defval=8, group=”General Settings”, minval=1, maxval=100, step=1, tooltip=”Number of neighbors to consider”),
input.int(title=”Max Bars Back”, defval=2000, group=”General Settings”, tooltip = “Use this to increase or reduce the range of backtesting. If script use much time to load, reduce this”),
input.int(title=”Feature Count”, defval=5, group=”Feature Engineering”, minval=2, maxval=5, tooltip=”Number of features to use for ML predictions.”),
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.”),
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”),
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”)
)

// Trade Stats Settings
// 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.
// showTradeStats = input.bool(false, ‘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”)
// 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”)

// Settings object for user-defined settings
FilterSettings filterSettings =
FilterSettings.new(
input.bool(title=”Use Volatility Filter”, defval=true, tooltip=”Whether to use the volatility filter.”, group=”Filters”),
input.bool(title=”Use Regime Filter”, defval=true, group=”Filters”, inline=”regime”),
input.bool(title=”Use ADX Filter”, defval=false, group=”Filters”, inline=”adx”),
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”),
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”)
)

// Filter object for filtering the ML predictions
Filter filter =
Filter.new(
ml.filter_volatility(1, 10, filterSettings.useVolatilityFilter),
ml.regime_filter(ohlc4, filterSettings.regimeThreshold, filterSettings.useRegimeFilter),
ml.filter_adx(settings.source, 14, filterSettings.adxThreshold, filterSettings.useAdxFilter)
)

// Feature Variables: User-Defined Inputs for calculating Feature Series.
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”)
f1_paramA = input.int(title=”Parameter A”, tooltip=”The primary parameter of feature 1.”, defval=14, inline = “02”, group=”Feature Engineering”)
f1_paramB = input.int(title=”Parameter B”, tooltip=”The secondary parameter of feature 2 (if applicable).”, defval=1, inline = “02”, group=”Feature Engineering”)
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”)
f2_paramA = input.int(title=”Parameter A”, tooltip=”The primary parameter of feature 2.”, defval=10, inline = “04”, group=”Feature Engineering”)
f2_paramB = input.int(title=”Parameter B”, tooltip=”The secondary parameter of feature 2 (if applicable).”, defval=11, inline = “04”, group=”Feature Engineering”)
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”)
f3_paramA = input.int(title=”Parameter A”, tooltip=”The primary parameter of feature 3.”, defval=20, inline = “06”, group=”Feature Engineering”)
f3_paramB = input.int(title=”Parameter B”, tooltip=”The secondary parameter of feature 3 (if applicable).”, defval=1, inline = “06”, group=”Feature Engineering”)
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”)
f4_paramA = input.int(title=”Parameter A”, tooltip=”The primary parameter of feature 4.”, defval=20, inline = “08”, group=”Feature Engineering”)
f4_paramB = input.int(title=”Parameter B”, tooltip=”The secondary parameter of feature 4 (if applicable).”, defval=2, inline = “08”, group=”Feature Engineering”)
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”)
f5_paramA = input.int(title=”Parameter A”, tooltip=”The primary parameter of feature 5.”, defval=9, inline = “10”, group=”Feature Engineering”)
f5_paramB = input.int(title=”Parameter B”, tooltip=”The secondary parameter of feature 5 (if applicable).”, defval=1, inline = “10”, group=”Feature Engineering”)

// FeatureSeries Object: Calculated Feature Series based on Feature Variables
featureSeries =
FeatureSeries.new(
series_from(f1_string, close, high, low, hlc3, f1_paramA, f1_paramB), // f1
series_from(f2_string, close, high, low, hlc3, f2_paramA, f2_paramB), // f2
series_from(f3_string, close, high, low, hlc3, f3_paramA, f3_paramB), // f3
series_from(f4_string, close, high, low, hlc3, f4_paramA, f4_paramB), // f4
series_from(f5_string, close, high, low, hlc3, f5_paramA, f5_paramB) // f5
)

// FeatureArrays Variables: Storage of Feature Series as Feature Arrays Optimized for ML
// Note: These arrays cannot be dynamically created within the FeatureArrays Object Initialization and thus must be set-up in advance.
var f1Array = array.new_float()
var f2Array = array.new_float()
var f3Array = array.new_float()
var f4Array = array.new_float()
var f5Array = array.new_float()
array.push(f1Array, featureSeries.f1)
array.push(f2Array, featureSeries.f2)
array.push(f3Array, featureSeries.f3)
array.push(f4Array, featureSeries.f4)
array.push(f5Array, featureSeries.f5)

// FeatureArrays Object: Storage of the calculated FeatureArrays into a single object
featureArrays =
FeatureArrays.new(
f1Array, // f1
f2Array, // f2
f3Array, // f3
f4Array, // f4
f5Array // f5
)

// Label Object: Used for classifying historical data as training data for the ML Model
Label direction_s =
Label.new(
long=1,
short=-1,
neutral=0
)

// Derived from General Settings
maxBarsBackIndex = last_bar_index >= settings.maxBarsBack ? last_bar_index – settings.maxBarsBack : 0

// EMA Settings
useEmaFilter = input.bool(title=”Use EMA Filter”, defval=false, group=”Filters”, inline=”ema”)
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.”)
isEmaUptrend = useEmaFilter ? close > ta.ema(close, emaPeriod) : true
isEmaDowntrend = useEmaFilter ? close < ta.ema(close, emaPeriod) : true useSmaFilter = input.bool(title=”Use SMA Filter”, defval=false, group=”Filters”, inline=”sma”) 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.”) isSmaUptrend = useSmaFilter ? close > ta.sma(close, smaPeriod) : true
isSmaDowntrend = useSmaFilter ? close < ta.sma(close, smaPeriod) : true

// Nadaraya-Watson Kernel Regression Settings
useKernelFilter = input.bool(true, “Trade with Kernel”, group=”Kernel Settings”, inline=”kernel”)
showKernelEstimate = input.bool(false, “Show Kernel Estimate”, group=”Kernel Settings”, inline=”kernel”)
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’)
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”)
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”)
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”)
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’)

// Display Settings
showBarColors = input.bool(false, “Show Bar Colors”, tooltip=”Whether to show the bar colors.”, group=”Display Settings”)
showBarPredictions = input.bool(defval = false, title = “Show Bar Prediction Values”, tooltip = “Will show the ML model’s evaluation of each bar as an integer.”, group=”Display Settings”)
useAtrOffset = input.bool(defval = false, title = “Use ATR Offset”, tooltip = “Will use the ATR offset instead of the bar prediction offset.”, group=”Display Settings”)
barPredictionsOffset = input.float(0, “Bar Prediction Offset”, minval=0, tooltip=”The offset of the bar predictions as a percentage from the bar high or close.”, group=”Display Settings”)

// =================================
// ==== Next Bar Classification ====
// =================================

// This model specializes specifically in predicting the direction_s of price action over the course of the next 4 bars.
// 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.
src = settings.source
y_train_series = src[4] < src[0] ? direction_s.short : src[4] > src[0] ? direction_s.long : direction_s.neutral
var y_train_array = array.new_int(0)

// Variables used for ML Logic
var predictions = array.new_float(0)
var prediction = 0.
var signal = direction_s.neutral
var distances = array.new_float(0)

array.push(y_train_array, y_train_series)

// =========================
// ==== Core ML Logic ====
// =========================

lastDistance = -1.0
size = math.min(settings.maxBarsBack-1, array.size(y_train_array)-1)
sizeLoop = math.min(settings.maxBarsBack-1, size)

if bar_index >= maxBarsBackIndex //{
for i = 0 to sizeLoop //{
d = get_lorentzian_distance(i, settings.featureCount, featureSeries, featureArrays)
if d >= lastDistance and i%4 //{
lastDistance := d
array.push(distances, d)
array.push(predictions, math.round(array.get(y_train_array, i)))
if array.size(predictions) > settings.neighborsCount //{
lastDistance := array.get(distances, math.round(settings.neighborsCount*3/4))
array.shift(distances)
array.shift(predictions)
//}
//}
//}
prediction := array.sum(predictions)
//}

// ============================
// ==== Prediction Filters ====
// ============================

// User Defined Filters: Used for adjusting the frequency of the ML Model’s predictions
filter_all = filter.volatility and filter.regime and filter.adx

// Filtered Signal: The model’s prediction of future price movement direction_s with user-defined filters applied
signal := prediction > 0 and filter_all ? direction_s.long : prediction < 0 and filter_all ? direction_s.short : nz(signal[1])

// Bar-Count Filters: Represents strict filters based on a pre-defined holding period of 4 bars
var int barsHeld = 0
barsHeld := ta.change(signal) ? 0 : barsHeld + 1
isHeldFourBars = barsHeld == 4
isHeldLessThanFourBars = 0 < barsHeld and barsHeld < 4

// Fractal Filters: Derived from relative ============ Appearance ============s of signals in a given time series fractal/segment with a default length of 4 bars
isDifferentSignalType = ta.change(signal)
isEarlySignalFlip = ta.change(signal) and (ta.change(signal[1]) or ta.change(signal[2]) or ta.change(signal[3]))
isBuySignal = signal == direction_s.long and isEmaUptrend and isSmaUptrend
isSellSignal = signal == direction_s.short and isEmaDowntrend and isSmaDowntrend
isLastSignalBuy = signal[4] == direction_s.long and isEmaUptrend[4] and isSmaUptrend[4]isLastSignalSell = signal[4] == direction_s.short and isEmaDowntrend[4] and isSmaDowntrend[4]isNewBuySignal = isBuySignal and isDifferentSignalType
isNewSellSignal = isSellSignal and isDifferentSignalType

// Kernel Regression Filters: Filters based on Nadaraya-Watson Kernel Regression using the Rational Quadratic Kernel
// For more information on this technique refer to my other open source indicator located here:
// https://www.tradingview.com/script/AWNvbPRM-Nadaraya-Watson-Rational-Quadratic-Kernel-Non-Repainting/
c_green = color.new(#009988, 20)
c_red = color.new(#CC3311, 20)
transparent = color.new(#000000, 100)
yhat1 = kernels.rationalQuadratic(settings.source, h, r, x)
yhat2 = kernels.gaussian(settings.source, h-lag, x)
kernelEstimate = yhat1
// Kernel Rates of Change
bool wasBearishRate = yhat1[2] > yhat1[1]bool wasBullishRate = yhat1[2] < yhat1[1] bool isBearishRate = yhat1[1] > yhat1
bool isBullishRate = yhat1[1] < yhat1 isBearishChange = isBearishRate and wasBullishRate isBullishChange = isBullishRate and wasBearishRate // Kernel Crossovers bool isBullishCrossAlert = ta.crossover(yhat2, yhat1) bool isBearishCrossAlert = ta.crossunder(yhat2, yhat1) bool isBullishSmooth = yhat2 >= yhat1
bool isBearishSmooth = yhat2 <= yhat1
// Kernel Colors
color colorByCross = isBullishSmooth ? c_green : c_red
color colorByRate = isBullishRate ? c_green : c_red
color plotColor = showKernelEstimate ? (useKernelSmoothing ? colorByCross : colorByRate) : transparent
plot(kernelEstimate, color=plotColor, linewidth=2, title=”Kernel Regression Estimate”, display = display.all – display.price_scale – display.status_line)
// Alert Variables
bool alertBullish = useKernelSmoothing ? isBullishCrossAlert : isBullishChange
bool alertBearish = useKernelSmoothing ? isBearishCrossAlert : isBearishChange
// Bullish and Bearish Filters based on Kernel
isBullish = useKernelFilter ? (useKernelSmoothing ? isBullishSmooth : isBullishRate) : true
isBearish = useKernelFilter ? (useKernelSmoothing ? isBearishSmooth : isBearishRate) : true

// ===========================
// ==== Entries and Exits ====
// ===========================

// Entry Conditions: Booleans for ML Model Position Entries
startLongTrade = isNewBuySignal and isBullish and isEmaUptrend and isSmaUptrend
startShortTrade = isNewSellSignal and isBearish and isEmaDowntrend and isSmaDowntrend

// Dynamic Exit Conditions: Booleans for ML Model Position Exits based on Fractal Filters and Kernel Regression Filters
lastSignalWasBullish = ta.barssince(startLongTrade) < ta.barssince(startShortTrade) lastSignalWasBearish = ta.barssince(startShortTrade) < ta.barssince(startLongTrade) barsSinceRedEntry = ta.barssince(startShortTrade) barsSinceRedExit = ta.barssince(alertBullish) barsSinceGreenEntry = ta.barssince(startLongTrade) barsSinceGreenExit = ta.barssince(alertBearish) isValidShortExit = barsSinceRedExit > barsSinceRedEntry
isValidLongExit = barsSinceGreenExit > barsSinceGreenEntry
endLongTradeDynamic = (isBearishChange and isValidLongExit[1])
endShortTradeDynamic = (isBullishChange and isValidShortExit[1])

// Fixed Exit Conditions: Booleans for ML Model Position Exits based on a Bar-Count Filters
endLongTradeStrict = ((isHeldFourBars and isLastSignalBuy) or (isHeldLessThanFourBars and isNewSellSignal and isLastSignalBuy)) and startLongTrade[4]endShortTradeStrict = ((isHeldFourBars and isLastSignalSell) or (isHeldLessThanFourBars and isNewBuySignal and isLastSignalSell)) and startShortTrade[4]isDynamicExitValid = not useEmaFilter and not useSmaFilter and not useKernelSmoothing
endLongTrade = settings.useDynamicExits and isDynamicExitValid ? endLongTradeDynamic : endLongTradeStrict
endShortTrade = settings.useDynamicExits and isDynamicExitValid ? endShortTradeDynamic : endShortTradeStrict

// =========================
// ==== Plotting Labels ====
// =========================

// 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.
//plotshape(startLongTrade ? low : na, ‘Buy’, shape.labelup, location.belowbar, color=ml.color_green(prediction), size=size.small, offset=0, display = display.all – display.price_scale – display.status_line)
//plotshape(startShortTrade ? high : na, ‘Sell’, shape.labeldown, location.abovebar, ml.color_red(-prediction), size=size.small, offset=0, display = display.all – display.price_scale – display.status_line)
//plotshape(endLongTrade and settings.showExits ? high : na, ‘StopBuy’, shape.xcross, location.absolute, color=#3AFF17, size=size.tiny, offset=0, display = display.all – display.price_scale – display.status_line)
//plotshape(endShortTrade and settings.showExits ? low : na, ‘StopSell’, shape.xcross, location.absolute, color=#FD1707, size=size.tiny, offset=0, display = display.all – display.price_scale – display.status_line)

// ================
// ==== Alerts ====
// ================

// Separate Alerts for Entries and Exits
//alertcondition(startLongTrade, title=’Open Long ▲’, message=’LDC Open Long ▲ | {{ticker}}@{{close}} | ({{interval}})’)
//alertcondition(endLongTrade, title=’Close Long ▲’, message=’LDC Close Long ▲ | {{ticker}}@{{close}} | ({{interval}})’)
//alertcondition(startShortTrade, title=’Open Short ▼’, message=’LDC Open Short | {{ticker}}@{{close}} | ({{interval}})’)
//alertcondition(endShortTrade, title=’Close Short ▼’, message=’LDC Close Short ▼ | {{ticker}}@{{close}} | ({{interval}})’)
//
//// Combined Alerts for Entries and Exits
//alertcondition(startShortTrade or startLongTrade, title=’Open Position ▲▼’, message=’LDC Open Position ▲▼ | {{ticker}}@{{close}} | ({{interval}})’)
//alertcondition(endShortTrade or endLongTrade, title=’Close Position ▲▼’, message=’LDC Close Position ▲▼ | {{ticker}}@[{{close}}] | ({{interval}})’)
//
//// Kernel Estimate Alerts
//alertcondition(condition=alertBullish, title=’Kernel Bullish Color Change’, message=’LDC Kernel Bullish ▲ | {{ticker}}@{{close}} | ({{interval}})’)
//alertcondition(condition=alertBearish, title=’Kernel Bearish Color Change’, message=’LDC Kernel Bearish ▼ | {{ticker}}@{{close}} | ({{interval}})’)

// =========================
// ==== Display Signals ====
// =========================

atrSpaced = useAtrOffset ? ta.atr(1) : na
compressionFactor = settings.neighborsCount / settings.colorCompression
c_pred = prediction > 0 ? color.from_gradient(prediction, 0, compressionFactor, #787b86, #009988) : prediction <= 0 ? color.from_gradient(prediction, -compressionFactor, 0, #CC3311, #787b86) : na c_label = showBarPredictions ? c_pred : na c_bars = showBarColors ? color.new(c_pred, 50) : na x_val = bar_index y_val = useAtrOffset ? prediction > 0 ? high + atrSpaced: low – atrSpaced : prediction > 0 ? high + hl2barPredictionsOffset/20 : low – hl2barPredictionsOffset/30
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)
barcolor(showBarColors ? color.new(c_pred, 50) : na)
// ==============================================================================================================================================================================================================================================================================================
// ====================================== Strategy code ================================================================

// —— Inputs for calculating our amount position ——–

initial_actual_capital = input.float(defval=10000, title = “Enter initial/current capital”, group = “============ Position amount calculator ===========”)
risk_c = input.float(2.5, ‘% account risk per trade’, step=1, group = “============ Position amount calculator ===========”, tooltip = “Percentage of total account to risk per trade. The USD value that should be used to risk the inserted percentage of the account. Appears as a yellow number in the upper left corner”)

// —— Date filter ———

initial_date = input.time(title=”Initial date”, defval=timestamp(“10 Feb 2014 13:30 +0000”), group=”============ Time filter ============ “, tooltip=”Enter the start date and time of the strategy”)
final_date = input.time(title=”Final date”, defval=timestamp(“01 Jan 2030 19:30 +0000”), group=”============ Time filter ============ “, tooltip=”Enter the end date and time of the strategy”)
dateFilter(int st, int et) => time >= st and time <= et
colorDate = input.bool(defval=false, title=”Date background”, tooltip = “Add color to the period of time of the strategy tester”)
bgcolor(colorDate and dateFilter(initial_date, final_date) ? color.new(color.blue, transp=90) : na)
date = dateFilter(initial_date, final_date)

// —— Session limits ——

timeSession = input.session(title=”Time session”, defval=”0000-2400″, group=”============ Time filter ============ “, tooltip=”Session time to operate. It may be different depending on your time zone, you have to find the correct hours manually.”)
colorBG = input.bool(title=”Session background”, defval=false, tooltip = “Add color to session time background”)
inSession(sess) => na(time(timeframe.period, sess + ‘:1234567’)) == false
bgcolor(inSession(timeSession) and colorBG ? color.rgb(0, 38, 255, 84) : na)

// ===============================================================================================================================================
// ———— Super Trend ———-

atrPeriod = input(9, “ATR Length SuperTrend”, group = “========= Super Trend filter ==========”)
factor = input.float(2.5, “Factor SuperTrend”, step = 0.05, group = “========= Super Trend filter ==========”)
[supertrend, direction_supertrend] = ta.supertrend(factor, atrPeriod)
show_supertrend = input.bool(defval = false, title=”Show supertrend ?”, group = “============ Appearance ============”)
bodyMiddle = plot(show_supertrend ? ((open + close) / 2) : na, display=display.none)
upTrend = plot(show_supertrend and direction_supertrend < 0 ? supertrend : na, “Up Trend”, color = color.green, style=plot.style_linebr, display = display.all – display.status_line) downTrend = plot(show_supertrend and direction_supertrend > 0 ? supertrend : na, “Down Trend”, color = color.red, style=plot.style_linebr, display = display.all – display.status_line)
fill(bodyMiddle, upTrend, color.new(color.green, 95), fillgaps=false, title = “Supertrend background”)
fill(bodyMiddle, downTrend, color.new(color.red, 95), fillgaps=false, title = “Supertrend background”)
up_trend_plot = direction_supertrend < 0 down_trend_plot = direction_supertrend > 0

// ———– Ema ———————-

ema = input.int(200, title=’Ema length’, minval=1, maxval=500, group = “============ Trend ============”)
ema200 = ta.ema(close, ema)
bullish = close > ema200
bearish = close < ema200
show_ema = input.bool(defval=true, title=”Show ema ?”, group = “============ Appearance ============”)
plot(show_ema ? ema200 : na, title = “Ema”, color=color.white, linewidth=2, display = display.all – display.status_line – display.price_scale)

// ===============================================================================================================================================
// ————– Atr stop loss by garethyeo (modified) —————–

source_atr = input(close, title=’Source’, group = “================== Stop loss ==================”, inline = “A”)
length_atr = input.int(14, minval=1, title=’Period’, group = “================== Stop loss ==================” , inline = “A”)
multiplier = input.float(1.5, minval=0.1, step=0.1, title=’Atr multiplier’, group = “================== Stop loss ==================”, inline = “A”, tooltip = “Defines the stop loss distance based on the Atr stop loss indicator”)
show_stoploss = input.bool(defval = true, title = “Show stop loss ?”, group = “============ Appearance ============”)
var float shortStopLoss = na
var float longStopLoss = na
var float atr_past_candle_long = na
var float atr_past_candle_short = na
candle_of_stoploss = input.string(defval = “Current candle”, title = “Stop loss source for atr stoploss”, group = “================== Stop loss ==================”, options = [“Current candle”,”Past candle”])
if candle_of_stoploss == “Current candle”
shortStopLoss := source_atr + ta.atr(length_atr) * multiplier
longStopLoss := source_atr – ta.atr(length_atr) * multiplier
if candle_of_stoploss == “Past candle”
shortStopLoss := close[1] + ta.atr(length_atr)[1] * multiplier[1]longStopLoss := close[1] – ta.atr(length_atr)[1] * multiplier[1]

// ——— Stop loss based in last swing high/low ——–

high_bars = input.int(defval = 10, title = “Highest price bars: “, group = “================== Stop loss ==================”)
low_bars = input.int(defval = 10, title = “Lowest price bars: “, group = “================== Stop loss ==================”)
stop_high = ta.highest(high, high_bars)
stop_low = ta.lowest(low, low_bars)

// ——— Stop loss source selection —————

stoploss_type = input.string(defval = “Atr stop loss”, title = “General stop loss source”, group = “================== Stop loss ==================”, options = [“Atr stop loss”,”Swing high/low”])
if stoploss_type == “Atr stop loss”
shortStopLoss := source_atr + ta.atr(length_atr) * multiplier
longStopLoss := source_atr – ta.atr(length_atr) * multiplier
if candle_of_stoploss == “Past candle” and stoploss_type == “Atr stop loss”
shortStopLoss := close[1] + ta.atr(length_atr)[1] * multiplier[1]longStopLoss := close[1] – ta.atr(length_atr)[1] * multiplier[1]if stoploss_type == “Swing high/low”
shortStopLoss := stop_high
longStopLoss := stop_low

// ======================================== Add/withdraw money frequently (>>>Beta<<<) ==============================================================

// Declare some variables =========================================================

var initial_capital = strategy.initial_capital
var initial_capital_a = strategy.initial_capital
var initial_capital_w = strategy.initial_capital
var float capital_added = 0
var float balance = strategy.initial_capital
var prev_month = 0
var float amount = 0
var add_frequency = 0
var withdraw_frequency = 0

// Choose how often the strategy adds money =========================================================

add_money_frequency = input.string(“Monthly”, title = “Choose how often would you add money”, options = [“Monthly”,”Weekly”,”Daily”,”Yearly”], group = “=============== Adding money frequently ===============”)
amount_to_add = input.float(defval = 10, title = “How much you want to add ?”, group = “=============== Adding money frequently ===============”)

if add_money_frequency == “Monthly”
add_frequency := month
if add_money_frequency == “Weekly”
add_frequency := weekofyear
if add_money_frequency == “Daily”
add_frequency := dayofweek
if add_money_frequency == “Yearly”
add_frequency := year

// Choose if you want to add money or not ====================================

add_money = input.string(“No”, title = “Add money from time to time ?”, options = [“Yes”,”No”] , group = “=============== Adding money frequently ===============”)

if add_frequency != add_frequency[1] and add_money == “Yes” and date
initial_capital_a += amount_to_add
initial_capital += amount_to_add
balance := strategy.netprofit + initial_capital

// Choose how often the strategy withdraws money =========================================================

amount_to_withdraw = input.string(“Fixed”, title = “Withdraw based in fixed amounts or percentage of earnings”, options = [“%”,”Fixed”], group = “=============== Withdraw money frequently ===============”)
amount_for_withdraw = input.float(defval = 2, title = “How much you want to withdraw (fixed amount) ?”, group = “=============== Withdraw money frequently ===============”)
withdraw_money_frequency = input.string(“Monthly”, title = “Choose how often would you withdraw money”, options = [“Monthly”,”Weekly”,”Daily”,”Yearly”], group = “=============== Withdraw money frequently ===============”)

// We use this for being able to choose the frequency of withdrawing money:
if withdraw_money_frequency == “Monthly”
withdraw_frequency := month
if withdraw_money_frequency == “Weekly”
withdraw_frequency := weekofyear
if withdraw_money_frequency == “Daily”
withdraw_frequency := dayofweek
if withdraw_money_frequency == “Yearly”
withdraw_frequency := year

// Choose if you want to withdraw money or not =============================

withdraw_money = input.string(“No”, title = “Withdraw money from time to time ?”, options = [“Yes”,”No”], group = “=============== Withdraw money frequently ===============”)
percentage_of_earnings = input.float(10, title = “Percentage of earnings”, group = “=============== Withdraw money frequently ===============”, tooltip = “Use this if withdraw is based in % of earnings”)

// Percentage of earnings:
if withdraw_frequency != withdraw_frequency[1] and withdraw_money == “Yes” and amount_to_withdraw == “%” and date and strategy.netprofit>0
initial_capital_w -= strategy.netprofit * (percentage_of_earnings / 100)
initial_capital -= strategy.netprofit * (percentage_of_earnings / 100)
balance := strategy.netprofit + initial_capital
// Fixed amount:
if withdraw_frequency != withdraw_frequency[1] and withdraw_money == “Yes” and amount_to_withdraw == “Fixed” and date and strategy.netprofit>0
initial_capital_w -= amount_for_withdraw
initial_capital -= amount_for_withdraw
balance := strategy.netprofit + initial_capital

// Logic

if withdraw_money == “Yes” and add_money == “No”
amount := balance
if withdraw_money ==”Yes” and add_money ==”Yes”
amount := balance
if withdraw_money == “No” and add_money == “Yes”
amount := balance
if withdraw_money == “No” and add_money == “No”
amount := strategy.equity

// ===============================================================================================================================================
// ————- Money management ————–

strategy_contracts = amount / close
distance_sl_long = -1 * (longStopLoss – close) / close
distance_sl_short = (shortStopLoss – close) / close
risk = input.float(2.5, ‘% Account risk per trade for backtesting’, step=1, group = “============ Risk management for trades ============”, tooltip = “Percentage of total account to risk per trade if fixed amounts is deactivated”)
long_amount = strategy_contracts * (risk / 100) / distance_sl_long
short_amount = strategy_contracts * (risk / 100) / distance_sl_short

leverage=input.bool(defval=true, title=”Use leverage for backtesting ?”, group = “============ Risk management for trades ============”, tooltip = “If it is activated, there will be no monetary units or amount of assets limit for each operation (That is, each operation will not be affected by the initial / current capital since it would be using leverage). If it is deactivated, the monetary units or the amount of assets to use for each operation will be limited by the initial/current capital.”)
if not leverage and long_amount>strategy_contracts
long_amount:=amount/close

if not leverage and short_amount>strategy_contracts
short_amount:=amount/close

// —- Fixed amounts —-

fixed_amounts = input.bool(defval = false, title = “Fixed amounts ?”, group = “============ Risk management for trades ============”, tooltip = “If you activate this, the backtester will use fixed amounts”)
fixed_amount_input = input.float(defval = 1000, title = “Fixed amount in usd”, group = “============ Risk management for trades ============”)
if fixed_amounts
long_amount := fixed_amount_input / close
short_amount := fixed_amount_input / close

// ———- Risk management —————

risk_reward_breakeven_long= input.float(title=”Risk/reward for breakeven long”, defval=1.0, step=0.1, group = “============ Risk management for trades ============”)
risk_reward_take_profit_long= input.float(title=”Risk/reward for take profit long”, defval=3.0, step=0.1, group = “============ Risk management for trades ============”)
risk_reward_breakeven_short= input.float(title=”Risk/reward for break even short”, defval=1.0, step=0.1, group = “============ Risk management for trades ============”)
risk_reward_take_profit_short= input.float(title=”Risk/reward for take profit short”, defval=3.0, step=0.1, group = “============ Risk management for trades ============”)
tp_percent=input.float(title=”% of trade for first take profit”, defval=50, step=5, group = “============ Risk management for trades ============”, tooltip = “Closing percentage of the current position when the first take profit is reached.”)

// ===============================================================================================================================================
// ———— Trade conditions —————
// Entry Conditions: Booleans for ML Model Position Entries
//startLongTrade = isNewBuySignal and isBullish and isEmaUptrend and isSmaUptrend
//startShortTrade = isNewSellSignal and isBearish and isEmaDowntrend and isSmaDowntrend
//endLongTrade = settings.useDynamicExits and isDynamicExitValid ? endLongTradeDynamic : endLongTradeStrict
//endShortTrade = settings.useDynamicExits and isDynamicExitValid ? endShortTradeDynamic : endShortTradeStrict
//bullish := close > ema200
//bearish := close < ema200 bought = strategy.position_size > 0
sold = strategy.position_size < 0
buy = startLongTrade
sell = startShortTrade
var float total_commissions_value = 0
var float commission_value_l = 0
var float commission_value_s = 0
var float sl_long = na
var float sl_short = na
var float be_long = na
var float be_short = na
var float tp_long = na
var float tp_short = na
var int totaltrades = 0

close_withsupertrend = input.bool(defval = true, title = “Close positions with supertrend ?”, group = “============ Positions management ============”)
closeshort_supertrend=ta.crossover(close, supertrend)
closelong_supertrend=ta.crossunder(close, supertrend)

if not bought
be_long:=na
sl_long:=na
tp_long:=na
if not sold
be_short:=na
sl_short:=na
tp_short:=na
long_positions = input.bool(defval = true, title = “Long positions ?”, group = “============ Positions management ============”)
short_positions = input.bool(defval = true, title = “Short positions ?”, group = “============ Positions management ============”)
use_takeprofit = input.bool(defval = true, title = “Use take profit ?”, group = “============ Risk management for trades ============”)
use_breakeven = input.bool(defval = true, title = “Use break even ?”, group = “============ Risk management for trades ============”)
close_only_tp = input.bool(defval = false, title = “Close just with take profit ?”, group = “============ Risk management for trades ============”, tooltip = “Activate if you just want to exit from a position until reaching take profit or stop loss. If it´s activated, change % of closing by tp to 100%”)
ema_filter_long = input.bool(defval = true, title = “Ema filter for long positions ?”, group = “============ Positions management ============”, tooltip = “Activate if you just want to long above 200 ema”)
ema_filter_short = input.bool(defval = true, title = “Ema filter for short positions ?”, group = “============ Positions management ============”, tooltip = “Activate if you just want to short under 200 ema”)
commission_percent = input.float(0.03, title = “Commission value in %”, group = “============ Positions management ============”, tooltip = “Set the % of commission. For example, when you enter into a position, you have a commission of 0.04% per entry and 0.04% per exit. You have also to change this value in properties for getting a real return in backtest. (in this case, 0.04%)”)

if fixed_amounts
commission_value_l := (close * (long_amount) * (commission_percent/100))
commission_value_s := (close * (short_amount) * (commission_percent/100))
if not fixed_amounts
commission_value_l := (close * ((strategy_contracts * (risk / 100)) / distance_sl_long) * (commission_percent/100))
commission_value_s := (close * ((strategy_contracts * (risk / 100)) / distance_sl_short) * (commission_percent/100))

// ======================= Strategy ===================================================================================================================

// Long position with take profit

if not bought and buy and date and long_positions and inSession(timeSession) and use_takeprofit and not ema_filter_long
sl_long:=longStopLoss
long_stoploss_distance = close – longStopLoss
be_long := close + long_stoploss_distance * risk_reward_breakeven_long
tp_long:=close+(long_stoploss_distance*risk_reward_take_profit_long)
total_commissions_value += commission_value_l
strategy.entry(‘L’, strategy.long, long_amount, alert_message = “Long”)
strategy.exit(“Tp”, “L”, stop=sl_long, limit=tp_long, qty_percent=tp_percent)
strategy.exit(‘Exit’, ‘L’, stop=sl_long)
if bought and high > be_long and use_breakeven
sl_long := strategy.position_avg_price
strategy.exit(“Tp”, “L”, stop=sl_long, limit=tp_long, qty_percent=tp_percent)
strategy.exit(‘Exit’, ‘L’, stop=sl_long)
if bought and sell and strategy.openprofit>0 and not close_only_tp or bought and closelong_supertrend and close_withsupertrend and strategy.openprofit>0 and not close_only_tp
strategy.close(“L”, comment=”CL”)
balance := balance + strategy.openprofit

// Long position without take profit

if not bought and buy and date and long_positions and inSession(timeSession) and not ema_filter_long
sl_long:=longStopLoss
long_stoploss_distance = close – longStopLoss
be_long := close + long_stoploss_distance * risk_reward_breakeven_long
total_commissions_value += commission_value_l
strategy.entry(‘L’, strategy.long, long_amount, alert_message = “Long”)
strategy.exit(‘Exit’, ‘L’, stop=sl_long)
if bought and high > be_long and use_breakeven
sl_long := strategy.position_avg_price
strategy.exit(‘Exit’, ‘L’, stop=sl_long)
if bought and sell and strategy.openprofit>0
strategy.close(“L”, comment=”CL”)
balance := balance + strategy.openprofit

// Short position with take profit

if not sold and sell and date and short_positions and inSession(timeSession) and use_takeprofit and not ema_filter_short
sl_short:=shortStopLoss
short_stoploss_distance=shortStopLoss – close
be_short:=((short_stoploss_distancerisk_reward_breakeven_short)-close)-1
tp_short:=((short_stoploss_distancerisk_reward_take_profit_short)-close)-1
total_commissions_value += commission_value_s
strategy.entry(“S”, strategy.short, short_amount, alert_message = “Short”)
strategy.exit(“Tp”, “S”, stop=sl_short, limit=tp_short, qty_percent=tp_percent)
strategy.exit(“Exit”, “S”, stop=sl_short)
if sold and low < be_short and use_breakeven sl_short:=strategy.position_avg_price strategy.exit(“Tp”, “S”, stop=sl_short, limit=tp_short, qty_percent=tp_percent) strategy.exit(“Exit”, “S”, stop=sl_short) if sold and buy and strategy.openprofit>0 and not close_only_tp or sold and closeshort_supertrend and close_withsupertrend and strategy.openprofit>0 and not close_only_tp
strategy.close(“S”, comment=”CS”)
balance := balance + strategy.openprofit

// Short position without take profit

if not sold and sell and date and short_positions and inSession(timeSession) and not ema_filter_short
sl_short:=shortStopLoss
short_stoploss_distance=shortStopLoss – close
be_short:=((short_stoploss_distancerisk_reward_breakeven_short)-close)-1
total_commissions_value += commission_value_s
strategy.entry(“S”, strategy.short, short_amount, alert_message = “Short”)
strategy.exit(“Exit”, “S”, stop=sl_short)
if sold and low < be_short and use_breakeven sl_short:=strategy.position_avg_price strategy.exit(“Exit”, “S”, stop=sl_short) if sold and buy and strategy.openprofit>0
strategy.close(“S”, comment=”CS”)
balance := balance + strategy.openprofit

// ===============================================================================================================================================

// Long position with ema filter

// With take profit

if not bought and buy and date and long_positions and inSession(timeSession) and use_takeprofit and bullish and ema_filter_long
sl_long:=longStopLoss
long_stoploss_distance = close – longStopLoss
be_long := close + long_stoploss_distance * risk_reward_breakeven_long
tp_long:=close+(long_stoploss_distance*risk_reward_take_profit_long)
total_commissions_value += commission_value_l
strategy.entry(‘L’, strategy.long, long_amount, alert_message = “Long”)
strategy.exit(“Tp”, “L”, stop=sl_long, limit=tp_long, qty_percent=tp_percent)
strategy.exit(‘Exit’, ‘L’, stop=sl_long)
if bought and high > be_long and use_breakeven
sl_long := strategy.position_avg_price
strategy.exit(“Tp”, “L”, stop=sl_long, limit=tp_long, qty_percent=tp_percent)
strategy.exit(‘Exit’, ‘L’, stop=sl_long)
if bought and sell and strategy.openprofit>0 and not close_only_tp or bought and closelong_supertrend and close_withsupertrend and strategy.openprofit>0 and not close_only_tp
strategy.close(“L”, comment=”CL”)
balance := balance + strategy.openprofit

// Without take profit

if not bought and buy and date and long_positions and inSession(timeSession) and bullish and ema_filter_long
sl_long:=longStopLoss
long_stoploss_distance = close – longStopLoss
be_long := close + long_stoploss_distance * risk_reward_breakeven_long
total_commissions_value += commission_value_l
strategy.entry(‘L’, strategy.long, long_amount, alert_message = “Long”)
strategy.exit(‘Exit’, ‘L’, stop=sl_long)
if bought and high > be_long and use_breakeven
sl_long := strategy.position_avg_price
strategy.exit(‘Exit’, ‘L’, stop=sl_long)
if bought and sell and strategy.openprofit>0
strategy.close(“L”, comment=”CL”)
balance := balance + strategy.openprofit

// Short positon with ema filter

// With take profit
if not sold and sell and date and short_positions and inSession(timeSession) and use_takeprofit and bearish and ema_filter_short
sl_short:=shortStopLoss
short_stoploss_distance=shortStopLoss – close
be_short:=((short_stoploss_distancerisk_reward_breakeven_short)-close)-1
tp_short:=((short_stoploss_distancerisk_reward_take_profit_short)-close)-1
total_commissions_value += commission_value_s
strategy.entry(“S”, strategy.short, short_amount, alert_message = “Short”)
strategy.exit(“Tp”, “S”, stop=sl_short, limit=tp_short, qty_percent=tp_percent)
strategy.exit(“Exit”, “S”, stop=sl_short)
if sold and low < be_short and use_breakeven sl_short:=strategy.position_avg_price strategy.exit(“Tp”, “S”, stop=sl_short, limit=tp_short, qty_percent=tp_percent) strategy.exit(“Exit”, “S”, stop=sl_short) if sold and buy and strategy.openprofit>0 and not close_only_tp or sold and closeshort_supertrend and close_withsupertrend and strategy.openprofit>0 and not close_only_tp
strategy.close(“S”, comment=”CS”)
balance := balance + strategy.openprofit

// Without take profit

if not sold and sell and date and short_positions and inSession(timeSession) and bearish and ema_filter_short
sl_short:=shortStopLoss
short_stoploss_distance=shortStopLoss – close
be_short:=((short_stoploss_distancerisk_reward_breakeven_short)-close)-1
total_commissions_value += commission_value_s
strategy.entry(“S”, strategy.short, short_amount, alert_message = “Short”)
strategy.exit(“Exit”, “S”, stop=sl_short)
if sold and low < be_short and use_breakeven sl_short:=strategy.position_avg_price strategy.exit(“Exit”, “S”, stop=sl_short) if sold and buy and strategy.openprofit>0
strategy.close(“S”, comment=”CS”)
balance := balance + strategy.openprofit

// ===============================================================================================================================================

// ———- Draw positions and signals on chart (strategy as an indicator) ————-

if high>tp_long
tp_long:=na
if lowbe_long
be_long:=na
if low<be_short
be_short:=na

show_position_on_chart = input.bool(defval=true, title=”Draw position on chart ?”, group = “============ Appearance ============”, tooltip = “Activate to graphically display profit, stop loss and break even”)
position_price = plot(show_position_on_chart? strategy.position_avg_price : na, style=plot.style_linebr, color = color.new(#ffffff, 10), linewidth = 1, display = display.all – display.status_line – display.price_scale)

sl_long_price = plot(show_position_on_chart and bought ? sl_long : na, style = plot.style_linebr, color = color.new(color.red, 10), linewidth = 1, display = display.all – display.status_line – display.price_scale)
sl_short_price = plot(show_position_on_chart and sold ? sl_short : na, style = plot.style_linebr, color = color.new(color.red, 10), linewidth = 1, display = display.all – display.status_line – display.price_scale)

tp_long_price = plot(bought and show_position_on_chart and use_takeprofit? tp_long : na, style = plot.style_linebr, color = color.new(#4cd350, 10), linewidth = 1, display = display.all – display.status_line – display.price_scale)
tp_short_price = plot(sold and show_position_on_chart and use_takeprofit? tp_short : na, style = plot.style_linebr, color = color.new(#4cd350, 10), linewidth = 1, display = display.all – display.status_line – display.price_scale)

breakeven_long = plot(bought and highbe_short and show_position_on_chart and use_breakeven? be_short : na , style = plot.style_linebr, color = color.new(#1fc9fd, 60), linewidth = 1, display = display.all – display.status_line – display.price_scale)

show_tpbe_on_chart = input.bool(defval=true, title=”Draw first take profit/breakeven price on chart ?”, group = “============ Appearance ============”, tooltip = “Activate to display take profit and breakeven price. It appears as a green point in the chart”)
long_stoploss_distance = close – longStopLoss
short_stoploss_distance=shortStopLoss – close
be_long_plot = close + long_stoploss_distance * risk_reward_breakeven_long
be_short_plot =((short_stoploss_distancerisk_reward_breakeven_short)-close)-1
tp_long_plot = close+(long_stoploss_distancerisk_reward_take_profit_long) tp_short_plot = ((short_stoploss_distancerisk_reward_take_profit_short)-close)*-1
plot(show_tpbe_on_chart and buy and use_breakeven and bullish and ema_filter_long or show_tpbe_on_chart and buy and use_breakeven and not ema_filter_long ? be_long_plot : na, color=color.new(#1fc9fd, 10), style = plot.style_circles, linewidth = 2, display = display.all – display.price_scale)
plot(show_tpbe_on_chart and sell and use_breakeven and bearish and ema_filter_short or show_tpbe_on_chart and sell and use_breakeven and not ema_filter_short ? be_short_plot : na, color=color.new(#1fc9fd, 10), style = plot.style_circles, linewidth = 2, display = display.all – display.price_scale)
plot(show_tpbe_on_chart and buy and use_takeprofit and bullish and ema_filter_long or show_tpbe_on_chart and buy and use_takeprofit and not ema_filter_long? tp_long_plot : na, color=color.new(#4cd350, 10), style = plot.style_circles, linewidth = 2, display = display.all – display.price_scale)
plot(show_tpbe_on_chart and sell and use_takeprofit and bearish and ema_filter_short or show_tpbe_on_chart and sell and use_takeprofit and not ema_filter_short? tp_short_plot : na, color=color.new(#4cd350, 10), style = plot.style_circles, linewidth = 2, display = display.all – display.price_scale)
plot(show_stoploss and buy and bullish and ema_filter_long or show_stoploss and buy and not ema_filter_long ? longStopLoss : na, title = “stop loss long”, color = color.white, style = plot.style_circles, linewidth = 2)
plot(show_stoploss and sell and bearish and ema_filter_short or show_stoploss and sell and not ema_filter_long ? shortStopLoss : na, title = “stop loss short”, color = color.white, style = plot.style_circles, linewidth = 2)

position_profit_long = plot(bought and show_position_on_chart and strategy.openprofit>0 ? close : na, style = plot.style_linebr, color = color.new(#4cd350, 10), linewidth = 1, display = display.all – display.status_line – display.price_scale)
position_profit_short = plot(sold and show_position_on_chart and strategy.openprofit>0 ? close : na, style = plot.style_linebr, color = color.new(#4cd350, 10), linewidth = 1, display = display.all – display.status_line – display.price_scale)

fill(plot1 = position_price, plot2 = position_profit_long, color = color.new(#4cd350, 90))
fill(plot1 = position_price, plot2 = position_profit_short, color = color.new(#4cd350, 90))

fill(plot1 = position_price, plot2 = sl_long_price, color = color.new(color.red,90))
fill(plot1 = position_price, plot2 = sl_short_price, color = color.new(color.red,90))

fill(plot1 = position_price, plot2 = tp_long_price, color = color.new(color.green,90))
fill(plot1 = position_price, plot2 = tp_short_price, color = color.new(color.green,90))

show_signals = input.bool(defval=true, title=”Show signals on chart ?”, group = “============ Appearance ============”)
plotshape(show_signals and buy and bullish and ema_filter_long or show_signals and buy and not ema_filter_long ? low : na, title=’Buy’, text=’Buy’, style=shape.labelup, location=location.belowbar, color=color.new(color.green, 20), textcolor=color.new(color.white, 0), size=size.tiny , display = display.all – display.price_scale – display.status_line)
plotshape(show_signals and sell and bearish and ema_filter_long or show_signals and sell and not ema_filter_short ? high : na, title=’Sell’, text=’Sell’, style=shape.labeldown, location=location.abovebar, color=color.new(color.red, 20), textcolor=color.new(color.white, 0), size=size.tiny, display = display.all – display.price_scale – display.status_line)
plotshape(show_signals and closelong_supertrend and close_withsupertrend and bought or show_signals and sell and bought ? low : na, title=’Cl Buy’, text=’Cl Buy’, style=shape.labelup, location=location.belowbar, color=color.new(#4cafaf, 30), textcolor=color.new(color.white, 0), size=size.tiny , display = display.all – display.price_scale – display.status_line)
plotshape(show_signals and closeshort_supertrend and close_withsupertrend and sold or show_signals and buy and bought? high : na, title=’Cl Buy’, text=’Cl sell’, style=shape.labeldown, location=location.abovebar, color=color.new(#4cafaf, 30), textcolor=color.new(color.white, 0), size=size.tiny, display = display.all – display.price_scale – display.status_line)

// ===============================================================================================================================================
// ————— Positions amount calculator ————-

contracts_amount_c = initial_actual_capital / close
distance_sl_long_c = -1 * (longStopLoss – close) / close
distance_sl_short_c = (shortStopLoss – close) / close
long_amount_c = close * (contracts_amount_c * (risk_c / 100) / distance_sl_long_c)
short_amount_c = close * (contracts_amount_c * (risk_c / 100) / distance_sl_short_c)
long_amount_lev = close * (contracts_amount_c * (risk_c / 100) / distance_sl_long_c)
short_amount_lev = close * (contracts_amount_c * (risk_c / 100) / distance_sl_short_c)

leverage_for_calculator=input.bool(defval=true, title=”Use leverage ?”, group = “============ Position amount calculator ===========”, tooltip = “If it is activated, there will be no monetary units or amount of assets limit for each operation (That is, each operation will not be affected by the initial / current capital since it would be using leverage). If it is deactivated, the monetary units or the amount of assets to use for each operation will be limited by the initial/current capital.”)

if not leverage_for_calculator and long_amount_lev>initial_actual_capital
long_amount_lev:=initial_actual_capital

if not leverage_for_calculator and short_amount_lev>initial_actual_capital
short_amount_lev:=initial_actual_capital

plot(buy and leverage_for_calculator ? long_amount_c : na, color = color.rgb(255, 230, 0), display = display.all – display.pane – display.price_scale)
plot(sell and leverage_for_calculator ? short_amount_c : na, color = color.rgb(255, 230, 0), display = display.all – display.pane – display.price_scale)
plot(buy and not leverage_for_calculator ? long_amount_lev : na, color = color.rgb(255, 230, 0), display = display.all – display.pane – display.price_scale)
plot(sell and not leverage_for_calculator ? short_amount_lev : na, color = color.rgb(255, 230, 0), display = display.all – display.pane – display.price_scale)

// ===============================================================================================================================================
// ===================== Drawing stats about add and withdraw money frequently and others on chart ===================

if not bought and buy and date and not ema_filter_long
totaltrades += 1
if not sold and sell and date and not ema_filter_short
totaltrades += 1
if not bought and buy and date and bearish and ema_filter_long
totaltrades += 0
if not sold and sell and date and bullish and ema_filter_short
totaltrades += 0
if not bought and buy and date and bullish and ema_filter_long
totaltrades += 1
if not sold and sell and date and bearish and ema_filter_short
totaltrades += 1

total_money_added = initial_capital_a – strategy.initial_capital
total_money_withdrawn = initial_capital_w – strategy.initial_capital
final_money = total_money_added + strategy.netprofit + strategy.initial_capital – total_money_withdrawn // or current money avaliable
// plot(commission_value_l, color = color.rgb(59, 245, 255), display = display.all – display.pane)
// plot(commission_value_s, color = color.rgb(59, 245, 255), display = display.all – display.pane)
// plot(total_commissions_value, color = color.rgb(252, 59, 255), display = display.all – display.pane)
// plot(total_trades/2, color = color.rgb(59, 245, 255), display = display.all – display.pane)
// plot(final_money, color = color.yellow, display = display.all – display.pane)
// plot(total_money_added, color = color.blue, display = display.all – display.pane)
// plot(total_money_withdrawn, color = color.red, display = display.all – display.pane)
// plot(strategy.netprofit, color = color.green, display = display.all – display.pane)

truncate(_number, _decimalPlaces) =>
_factor = math.pow(10, _decimalPlaces)
int(_number * _factor) / _factor

draw_stats = input.bool(true, “Show stats from add/withdraw money frequently ?”, group = “============ Appearance ============”)

// Prepare stats table

var table testTable = table.new(position.top_right, 5, 2, border_width=2, frame_color = color.rgb(0, 0, 0), frame_width = 2)
f_fillCell(_table, _column, _row, _title, _value, _bgcolor, _txtcolor,_size,_tooltip) =>
_cellText = _title + “\n” + _value
table.cell(_table, _column, _row, _cellText, bgcolor=_bgcolor, text_color=_txtcolor, text_size=_size, tooltip = _tooltip)

// Draw stats table

var bgcolor = color.new(#2f5cda, 50)
var bgcolor2 = color.new(color.green, 50)
var bgcolor3 = color.new(color.red,50)
if draw_stats
if barstate.islastconfirmedhistory
f_fillCell(testTable, 0, 0, “Final/current money:”, “$” + str.tostring(truncate(final_money,2)), bgcolor, color.white, _size = size.normal, _tooltip = “Total money added + total return from strategy + initial capital – total money withdrawn”)
f_fillCell(testTable, 0, 1, “Total money added:”, “$” + str.tostring(truncate(total_money_added,2)), bgcolor, color.white, _size = size.normal, _tooltip = “Sum of total money added at the end of the date”)
f_fillCell(testTable, 1, 0, “Total money withdrawn:”, “$” + str.tostring(truncate(total_money_withdrawn*-1,2)), bgcolor, color.white, _size = size.normal, _tooltip = “Sum of total money withdrawn at the end of the date”)
f_fillCell(testTable, 1, 1, “Total return:”, “$” + str.tostring(truncate(strategy.netprofit,2)), strategy.netprofit > 0 ? bgcolor2 : bgcolor3 , color.white, _size = size.normal, _tooltip = “Total return from strategy until the end of the date (it could be different from default backtesting if last position have not been closed completly)”)
f_fillCell(testTable, 2, 0, “Total trades:”, “” + str.tostring(truncate(totaltrades,2)), bgcolor, color.white, _size = size.normal, _tooltip = “Sum of real total trades. The value from default backtester it´s not precise”)
f_fillCell(testTable, 2, 1, “Total commissions value:”, “” + str.tostring(truncate(total_commissions_value,2)), bgcolor, color.white, _size = size.normal, _tooltip = “Sum of commissions value from all trades. You must set the % value in the script settings (Positions management).”)
// ===================================================================== 😀

Інструкції

  1. Встанови залежності (якщо ще немає):
pip install pandas numpy matplotlib

(усі функції написані вручну — додаткових C-бібліотек не треба)

  1. Збережи файл, наприклад lorentzian_strategy.py, і запусти:
python lorentzian_strategy.py path/to/ohlcv.csv

де ohlcv.csv — CSV з колонками: datetime,open,high,low,close,volume (datetime у форматі, що читає pandas.read_csv(..., parse_dates=[0])).


Повний код (скопіюй у файл lorentzian_strategy.py)

"""
lorentzian_strategy.py
Повна конвертація Pine Script -> Python (логіка Lorentzian Classification Strategy + risk management + backtester)
Author: generated by ChatGPT (adapted for user's Pine script)
"""

import sys
import numpy as np
import pandas as pd
from math import log
import matplotlib.pyplot as plt

# ----------------------------
# === Indicator helpers ===
# ----------------------------

def ema(series, period):
    return series.ewm(span=period, adjust=False).mean()

def sma(series, period):
    return series.rolling(period).mean()

def rsi(series, period=14):
    delta = series.diff()
    up = delta.clip(lower=0)
    down = -delta.clip(upper=0)
    ma_up = up.ewm(alpha=1/period, adjust=False).mean()
    ma_down = down.ewm(alpha=1/period, adjust=False).mean()
    rs = ma_up / ma_down
    return 100 - (100 / (1 + rs))

def cci(high, low, close, period=20):
    tp = (high + low + close) / 3
    ma = tp.rolling(period).mean()
    mad = tp.rolling(period).apply(lambda x: np.mean(np.abs(x - np.mean(x))), raw=True)
    # avoid division by zero
    denom = 0.015 * mad
    return (tp - ma) / denom

def atr(high, low, close, period=14):
    h_l = high - low
    h_pc = (high - close.shift(1)).abs()
    l_pc = (low - close.shift(1)).abs()
    tr = pd.concat([h_l, h_pc, l_pc], axis=1).max(axis=1)
    return tr.rolling(period).mean()

def adx(high, low, close, period=14):
    # Basic ADX implementation
    up_move = high.diff()
    down_move = -low.diff()
    plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
    minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
    tr = pd.concat([
        high - low,
        (high - close.shift(1)).abs(),
        (low - close.shift(1)).abs()
    ], axis=1).max(axis=1)
    atr_ = tr.rolling(period).mean()
    plus_di = 100 * (pd.Series(plus_dm).rolling(period).sum() / atr_)
    minus_di = 100 * (pd.Series(minus_dm).rolling(period).sum() / atr_)
    dx = (abs(plus_di - minus_di) / (plus_di + minus_di)) * 100
    adx = dx.rolling(period).mean()
    return adx.fillna(0)

def wave_trend(hlc3, n1=10, n2=21):
    # Simple WaveTrend implementation
    esa = hlc3.ewm(alpha=1/n1, adjust=False).mean()
    d = (hlc3 - esa).abs().ewm(alpha=1/n1, adjust=False).mean()
    ci = (hlc3 - esa) / (0.015 * d)
    tci = ci.ewm(alpha=1/n2, adjust=False).mean()
    return tci

def supertrend(df, period=10, multiplier=3.0):
    # returns supertrend series and direction (True=uptrend)
    hl2 = (df['high'] + df['low']) / 2
    atr_ = atr(df['high'], df['low'], df['close'], period)
    upperbasic = hl2 + multiplier * atr_
    lowerbasic = hl2 - multiplier * atr_
    upper = upperbasic.copy()
    lower = lowerbasic.copy()
    final_upper = upper.copy()
    final_lower = lower.copy()
    trend = pd.Series(True, index=df.index)  # True == up
    for i in range(1, len(df)):
        if upperbasic.iloc[i] < final_upper.iloc[i-1] or df['close'].iloc[i-1] > final_upper.iloc[i-1]:
            final_upper.iloc[i] = upperbasic.iloc[i]
        else:
            final_upper.iloc[i] = final_upper.iloc[i-1]
        if lowerbasic.iloc[i] > final_lower.iloc[i-1] or df['close'].iloc[i-1] < final_lower.iloc[i-1]:
            final_lower.iloc[i] = lowerbasic.iloc[i]
        else:
            final_lower.iloc[i] = final_lower.iloc[i-1]
        if trend.iloc[i-1] and df['close'].iloc[i] < final_upper.iloc[i]:
            trend.iloc[i] = False
        elif not trend.iloc[i-1] and df['close'].iloc[i] > final_lower.iloc[i]:
            trend.iloc[i] = True
        else:
            trend.iloc[i] = trend.iloc[i-1]
    st = pd.Series(np.where(trend, final_lower, final_upper), index=df.index)
    return st, trend  # st value, boolean uptrend

# ----------------------------
# === Kernel regressions ===
# ----------------------------

def gaussian_kernel(x, y, length_scale):
    d2 = (x - y) ** 2
    return np.exp(-0.5 * d2 / (length_scale ** 2))

def rational_quadratic_kernel(x, y, alpha, length_scale):
    # k = (1 + d^2 / (2*alpha*l^2))^{-alpha}
    d2 = (x - y) ** 2
    return (1 + d2 / (2 * alpha * (length_scale ** 2))) ** (-alpha)

def nadaraya_watson(series, window=8, kernel='rational', param_r=8.0, length_scale=1.0):
    """
    Compute kernel estimate yhat for each index using last `window` points.
    For simplicity we compute 1-step-ahead estimate at each i using points i-window+1..i
    Returns pd.Series of same length where first window-1 values are NaN.
    """
    n = len(series)
    yhat = pd.Series(index=series.index, dtype=float)
    xs = np.arange(n)  # time indices
    for i in range(window-1, n):
        idx = np.arange(i - window + 1, i + 1)
        x_sub = xs[idx]
        y_sub = series.iloc[idx].values
        x0 = xs[i]  # predict at current index
        if kernel == 'gaussian':
            w = gaussian_kernel(x_sub, x0, length_scale)
        else:
            w = rational_quadratic_kernel(x_sub, x0, param_r, length_scale)
        if w.sum() == 0:
            yhat.iloc[i] = np.nan
        else:
            yhat.iloc[i] = np.sum(w * y_sub) / np.sum(w)
    return yhat

# ----------------------------
# === Lorentzian ML core ===
# ----------------------------

def lorentzian_distance_vec(feature_row, historical_features):
    # feature_row: 1D array (f1..fN)
    # historical_features: 2D array shape (M, N)
    # compute sum(log(1+abs(diff))) along features
    diffs = np.abs(historical_features - feature_row)
    return np.sum(np.log1p(diffs), axis=1)

class LorentzianKNN:
    def __init__(self, neighbors=8, feature_count=5, max_bars_back=2000):
        self.neighbors = neighbors
        self.feature_count = feature_count
        self.max_bars_back = max_bars_back
        # stored history features and labels
        self.features_hist = []  # list of arrays
        self.labels_hist = []    # list of ints: 1, -1, 0

    def push(self, feature_row, label):
        self.features_hist.append(np.array(feature_row[:self.feature_count], dtype=float))
        self.labels_hist.append(int(label))

    def predict(self, feature_row):
        if len(self.features_hist) == 0:
            return 0
        hist = np.array(self.features_hist[-self.max_bars_back:])  # respect max_bars_back
        labs = np.array(self.labels_hist[-self.max_bars_back:])
        dists = lorentzian_distance_vec(np.array(feature_row[:self.feature_count], dtype=float), hist)
        # In Pine script they push distances and predictions, then keep only neighborsCount most recent (smallest?)...
        # The original logic: they accumulate distances >= lastDistance and keep window size = neighborsCount.
        # We'll use: take nearest K (smallest distances)
        k = min(self.neighbors, len(dists))
        ix = np.argsort(dists)[:k]
        pred_sum = labs[ix].sum()
        # prediction is integer sum, in Pine they used array.sum(predictions) — we mimic sign
        return np.sign(pred_sum) if pred_sum != 0 else 0

# ----------------------------
# === Backtester & Strategy ===
# ----------------------------

def build_features(df, params):
    """
    params: dict with f1..f5 selections and their parameters
    returns df with added columns f1..f5 and kernel estimates etc.
    """
    df = df.copy()
    df['hlc3'] = (df['high'] + df['low'] + df['close']) / 3.0
    # compute features according to string type
    def feature_series(feat_name, pA, pB):
        if feat_name == 'RSI':
            return rsi(df['close'], period=int(pA)).fillna(0)
        if feat_name == 'WT':
            return wave_trend(df['hlc3'], n1=int(pA), n2=int(pB)).fillna(0)
        if feat_name == 'CCI':
            return cci(df['high'], df['low'], df['close'], period=int(pA)).fillna(0)
        if feat_name == 'ADX':
            # use ADX with pA period
            return adx(df['high'], df['low'], df['close'], period=int(pA)).fillna(0)
        raise ValueError("Unknown feature")
    df['f1'] = feature_series(params['f1_string'], params['f1_paramA'], params['f1_paramB'])
    df['f2'] = feature_series(params['f2_string'], params['f2_paramA'], params['f2_paramB'])
    df['f3'] = feature_series(params['f3_string'], params['f3_paramA'], params['f3_paramB'])
    df['f4'] = feature_series(params['f4_string'], params['f4_paramA'], params['f4_paramB'])
    df['f5'] = feature_series(params['f5_string'], params['f5_paramA'], params['f5_paramB'])

    # EMA/SMA
    df['ema200'] = ema(df['close'], params.get('emaPeriod', 200))
    df['sma200'] = df['close'].rolling(params.get('smaPeriod', 200)).mean()

    # ATR / supertrend
    df['atr'] = atr(df['high'], df['low'], df['close'], period=params.get('atrPeriod', 9))
    st_val, st_trend = supertrend(df, period=params.get('atrPeriod', 9), multiplier=params.get('factor', 2.5))
    df['supertrend'] = st_val
    df['supertrend_dir'] = st_trend

    # kernel estimates (two kernels)
    df['yhat_rq'] = nadaraya_watson(df['close'], window=params.get('h', 8), kernel='rational', param_r=params.get('r', 8.0), length_scale=max(1.0, params.get('x', 25)))
    df['yhat_g'] = nadaraya_watson(df['close'], window=max(3, params.get('h', 8)-params.get('lag', 2)), kernel='gaussian', length_scale=max(1.0, params.get('x', 25)))
    return df

def generate_signals_and_backtest(df, params):
    """
    Main loop: builds training labels, runs LorentzianKNN, forms signals, applies filters and executes simulated trades.
    Returns trades list and df with prediction/signal columns.
    """
    # Create DataFrame copy
    df = df.copy().reset_index(drop=True)
    n = len(df)
    # label: compare price 4 bars ahead: df['close'].shift(-4) in pandas (we want next 4 bars)
    future_close = df['close'].shift(-4)
    label_series = np.where(future_close < df['close'], -1, np.where(future_close > df['close'], 1, 0))
    # prepare ML model
    model = LorentzianKNN(neighbors=params['neighborsCount'], feature_count=params['featureCount'], max_bars_back=params['maxBarsBack'])
    predictions = np.zeros(n, dtype=int)
    signal = np.zeros(n, dtype=int)  # final signal after filters
    # for storing trade state
    in_position = False
    pos = None
    trades = []
    # vars for barsHeld (count of same signal)
    last_signal = 0
    barsHeld = 0

    for i in range(n):
        # push historical training label for bar i (we follow Pine: they push y_train_array of current bar as they go)
        # in Pine they used array.push(y_train_array, y_train_series) at each bar, where y_train_series depends on src[4] vs src[0]
        # replicate: when i <= n-5, label_series[i] is known; else treat as 0
        if i <= n - 5:
            model.push([df.at[i, 'f1'], df.at[i, 'f2'], df.at[i, 'f3'], df.at[i, 'f4'], df.at[i, 'f5']], label_series[i])
        # Predict using current feature vector (features of current bar)
        feat_vec = [df.at[i, 'f1'], df.at[i, 'f2'], df.at[i, 'f3'], df.at[i, 'f4'], df.at[i, 'f5']]
        pred = model.predict(feat_vec)
        predictions[i] = pred

        # Filters
        # volatility filter (simple: require ATR > threshold) -> original used ml.filter_volatility(1,10, useVolatilityFilter) - ambiguous.
        # We'll implement a simple volatility filter: ATR over 14 greater than small threshold.
        vol_ok = df.at[i, 'atr'] > 0 if params['filterSettings_useVolatilityFilter'] else True
        # regime filter - use hlc3 trend (regimeThreshold default -0.1 in pine)
        hlc3 = df.at[i, 'hlc3']
        # simple regime: compare short ema vs long ema? We'll use kernel slope: yhat_rq rate of change
        regime_ok = True
        if params['filterSettings_useRegimeFilter']:
            thr = params['filterSettings_regimeThreshold']
            # use yhat_rq slope over 2 bars
            if i >= 2 and not np.isnan(df.at[i, 'yhat_rq']) and not np.isnan(df.at[i-1, 'yhat_rq']) and not np.isnan(df.at[i-2, 'yhat_rq']):
                was_bear = df.at[i-2, 'yhat_rq'] > df.at[i-1, 'yhat_rq']
                is_bear = df.at[i-1, 'yhat_rq'] > df.at[i, 'yhat_rq']
                # treat thr simply: if thr < 0 => allow both
                regime_ok = True if thr <= 0 else (not is_bear)
        # adx filter:
        adx_ok = True
        if params['filterSettings_useAdxFilter']:
            current_adx = adx(df['high'], df['low'], df['close'], period=14).iloc[i]
            adx_ok = current_adx >= params['filterSettings_adxThreshold']

        filter_all = vol_ok and regime_ok and adx_ok

        # compute EMA/SMA trend flags
        isEmaUp = df.at[i, 'close'] > df.at[i, 'ema200'] if params['useEmaFilter'] else True
        isSmaUp = df.at[i, 'close'] > df.at[i, 'sma200'] if params['useSmaFilter'] else True
        isEmaDown = df.at[i, 'close'] < df.at[i, 'ema200'] if params['useEmaFilter'] else True
        isSmaDown = df.at[i, 'close'] < df.at[i, 'sma200'] if params['useSmaFilter'] else True

        # kernel bullish/bearish detection
        # replicate Pine: yhat1 = rq, yhat2 = gaussian(h-lag)
        useKernelSmoothing = params['useKernelSmoothing']
        yhat1 = df.at[i, 'yhat_rq']
        yhat2 = df.at[i, 'yhat_g']
        isBullishRate = False
        isBearishRate = False
        if not (np.isnan(yhat1) or np.isnan(yhat1 if i-1 < 0 else df.at[i-1, 'yhat_rq'])):
            # simple rate comparisons
            if i >= 2 and not np.isnan(df.at[i-2, 'yhat_rq']):
                wasBearishRate = df.at[i-2, 'yhat_rq'] > df.at[i-1, 'yhat_rq']
                wasBullishRate = df.at[i-2, 'yhat_rq'] < df.at[i-1, 'yhat_rq']
                isBearishRate = df.at[i-1, 'yhat_rq'] > df.at[i, 'yhat_rq'] if i >= 1 else False
                isBullishRate = df.at[i-1, 'yhat_rq'] < df.at[i, 'yhat_rq'] if i >= 1 else False
        isBullish = True
        isBearish = True
        if params['useKernelFilter']:
            isBullish = ( (yhat2 >= yhat1) if useKernelSmoothing else isBullishRate ) if not np.isnan(yhat1) and not np.isnan(yhat2) else True
            isBearish = ( (yhat2 <= yhat1) if useKernelSmoothing else isBearishRate ) if not np.isnan(yhat1) and not np.isnan(yhat2) else True

        # combine pred and filters
        sig = 0
        if pred > 0 and filter_all and isBullish and isEmaUp and isSmaUp:
            sig = 1
        elif pred < 0 and filter_all and isBearish and isEmaDown and isSmaDown:
            sig = -1
        else:
            # carry previous signal (nz(signal[1]) in Pine)
            sig = signal[i-1] if i>0 else 0

        signal[i] = sig

        # barsHeld & fractal filters:
        if i==0:
            barsHeld = 0
        else:
            barsHeld = 0 if sig != last_signal else barsHeld + 1
        last_signal = sig

        # Entry conditions:
        isNewBuySignal = (sig == 1) and (i==0 or signal[i-1] != 1)
        isNewSellSignal = (sig == -1) and (i==0 or signal[i-1] != -1)

        # compute stoploss values
        # atr-based stoploss or swing high/low
        candle_of_stoploss = params.get('candle_of_stoploss', 'Current candle')
        if params['stoploss_type'] == 'Atr stop loss':
            if candle_of_stoploss == 'Current candle':
                sl_short = df.at[i, 'close'] + df.at[i, 'atr'] * params.get('multiplier', 1.5)
                sl_long  = df.at[i, 'close'] - df.at[i, 'atr'] * params.get('multiplier', 1.5)
            else:
                # past candle
                j = max(0, i-1)
                sl_short = df.at[j, 'close'] + df.at[j, 'atr'] * params.get('multiplier', 1.5)
                sl_long  = df.at[j, 'close'] - df.at[j, 'atr'] * params.get('multiplier', 1.5)
        else:
            # swing high/low based on lookback
            hb = params.get('high_bars', 10)
            lb = params.get('low_bars', 10)
            stop_high = df['high'].rolling(hb).max().iloc[i]
            stop_low = df['low'].rolling(lb).min().iloc[i]
            sl_short = stop_high
            sl_long = stop_low

        # Position sizing approx (uses risk % and distance to SL)
        # contracts_amount = amount / close  (we keep amount = strategy.equity substitute with capital)
        capital = params.get('initial_actual_capital', 10000)
        contracts_amount = capital / df.at[i, 'close'] if df.at[i, 'close']>0 else 0.0
        # distance_sl_long = -1 * (longStopLoss - close) / close
        distance_sl_long = -1 * (sl_long - df.at[i, 'close']) / df.at[i, 'close'] if sl_long is not None else 0.0
        distance_sl_short = (sl_short - df.at[i, 'close']) / df.at[i, 'close'] if sl_short is not None else 0.0
        risk_pct = params.get('risk', 2.5) / 100.0
        long_amount = contracts_amount * (risk_pct) / (distance_sl_long if distance_sl_long!=0 else 1e-8)
        short_amount = contracts_amount * (risk_pct) / (distance_sl_short if distance_sl_short!=0 else 1e-8)

        # --- EXECUTE TRADES (simple discrete-time backtest) ---
        # Close logic: if in_position, check stoploss/takeprofit hits at bar i (use bar high/low)
        if in_position:
            # check longs
            if pos['side'] == 'long':
                # stop loss hit?
                if df.at[i, 'low'] <= pos['stop']:
                    # close at stop price
                    exit_price = pos['stop']
                    trades.append({
                        'entry_idx': pos['entry_idx'],
                        'exit_idx': i,
                        'side': 'long',
                        'entry_price': pos['entry_price'],
                        'exit_price': exit_price,
                        'pnl': (exit_price - pos['entry_price']) * pos['qty'] - params.get('commission_percent',0.03)/100.0 * (exit_price*pos['qty'] + pos['entry_price']*pos['qty'])
                    })
                    in_position = False
                    pos = None
                # take profit
                elif pos.get('tp') is not None and df.at[i, 'high'] >= pos['tp']:
                    exit_price = pos['tp']
                    trades.append({
                        'entry_idx': pos['entry_idx'],
                        'exit_idx': i,
                        'side': 'long',
                        'entry_price': pos['entry_price'],
                        'exit_price': exit_price,
                        'pnl': (exit_price - pos['entry_price']) * pos['qty'] - params.get('commission_percent',0.03)/100.0 * (exit_price*pos['qty'] + pos['entry_price']*pos['qty'])
                    })
                    in_position = False
                    pos = None
            else:
                # short
                if df.at[i, 'high'] >= pos['stop']:
                    exit_price = pos['stop']
                    trades.append({
                        'entry_idx': pos['entry_idx'],
                        'exit_idx': i,
                        'side': 'short',
                        'entry_price': pos['entry_price'],
                        'exit_price': exit_price,
                        'pnl': (pos['entry_price'] - exit_price) * pos['qty'] - params.get('commission_percent',0.03)/100.0 * (exit_price*pos['qty'] + pos['entry_price']*pos['qty'])
                    })
                    in_position = False
                    pos = None
                elif pos.get('tp') is not None and df.at[i, 'low'] <= pos['tp']:
                    exit_price = pos['tp']
                    trades.append({
                        'entry_idx': pos['entry_idx'],
                        'exit_idx': i,
                        'side': 'short',
                        'entry_price': pos['entry_price'],
                        'exit_price': exit_price,
                        'pnl': (pos['entry_price'] - exit_price) * pos['qty'] - params.get('commission_percent',0.03)/100.0 * (exit_price*pos['qty'] + pos['entry_price']*pos['qty'])
                    })
                    in_position = False
                    pos = None

        # If not in position, consider new entries
        if not in_position:
            if isNewBuySignal and params['long_positions'] and (not params['useEmaFilter'] or isEmaUp) and (not params['useSmaFilter'] or isSmaUp) and df.at[i, 'close']>0:
                # open long
                qty = long_amount
                entry_price = df.at[i, 'close']
                tp = None
                if params['use_takeprofit']:
                    long_stoploss_distance = entry_price - sl_long
                    tp = entry_price + long_stoploss_distance * params.get('risk_reward_take_profit_long', 3.0)
                pos = {
                    'side': 'long',
                    'entry_price': entry_price,
                    'qty': qty,
                    'stop': sl_long,
                    'tp': tp,
                    'entry_idx': i
                }
                in_position = True
            elif isNewSellSignal and params['short_positions'] and (not params['useEmaFilter'] or isEmaDown) and (not params['useSmaFilter'] or isSmaDown) and df.at[i, 'close']>0:
                qty = short_amount
                entry_price = df.at[i, 'close']
                tp = None
                if params['use_takeprofit']:
                    short_stoploss_distance = sl_short - entry_price
                    tp = entry_price - short_stoploss_distance * params.get('risk_reward_take_profit_short', 3.0)
                pos = {
                    'side': 'short',
                    'entry_price': entry_price,
                    'qty': qty,
                    'stop': sl_short,
                    'tp': tp,
                    'entry_idx': i
                }
                in_position = True

    # convert trades to DataFrame
    trades_df = pd.DataFrame(trades)
    return predictions, signal, trades_df

# ----------------------------
# === Example usage ===
# ----------------------------

def default_params():
    return {
        'neighborsCount': 8,
        'maxBarsBack': 2000,
        'featureCount': 5,
        'colorCompression': 1,
        'useEmaFilter': False,
        'useSmaFilter': False,
        'useKernelFilter': True,
        'useKernelSmoothing': False,
        'h': 8,
        'r': 8.0,
        'x': 25,
        'lag': 2,
        'f1_string': 'RSI', 'f1_paramA': 14, 'f1_paramB': 1,
        'f2_string': 'WT', 'f2_paramA': 10, 'f2_paramB': 11,
        'f3_string': 'CCI', 'f3_paramA': 20, 'f3_paramB': 1,
        'f4_string': 'ADX', 'f4_paramA': 20, 'f4_paramB': 2,
        'f5_string': 'RSI', 'f5_paramA': 9,  'f5_paramB': 1,
        # filters
        'filterSettings_useVolatilityFilter': True,
        'filterSettings_useRegimeFilter': True,
        'filterSettings_useAdxFilter': False,
        'filterSettings_regimeThreshold': -0.1,
        'filterSettings_adxThreshold': 20,
        # stoploss / money mgmt
        'atrPeriod': 9,
        'factor': 2.5,
        'candle_of_stoploss': 'Current candle',
        'stoploss_type': 'Atr stop loss',  # or 'Swing high/low'
        'high_bars': 10,
        'low_bars': 10,
        'initial_actual_capital': 10000,
        'risk': 2.5,
        'risk_reward_breakeven_long': 1.0,
        'risk_reward_take_profit_long': 3.0,
        'risk_reward_breakeven_short': 1.0,
        'risk_reward_take_profit_short': 3.0,
        'tp_percent': 50,
        'use_takeprofit': True,
        'use_breakeven': True,
        'long_positions': True,
        'short_positions': True,
        'commission_percent': 0.03,
        # plotting / others
        'useKernelSmoothing': False,
    }

def simple_metrics(trades_df):
    if trades_df.empty:
        return {}
    pnl = trades_df['pnl']
    return {
        'trades': len(trades_df),
        'gross_pnl': pnl.sum(),
        'avg_pnl': pnl.mean(),
        'win_rate': (pnl > 0).mean(),
        'max_drawdown': None
    }

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python lorentzian_strategy.py path/to/ohlcv.csv")
        sys.exit(1)
    path = sys.argv[1]
    df = pd.read_csv(path, parse_dates=[0])
    # expect columns datetime, open, high, low, close, volume
    df.columns = [c.strip().lower() for c in df.columns]
    # rename to required
    rename_map = {}
    if 'datetime' in df.columns:
        rename_map['datetime'] = 'datetime'
    if 'open' not in df.columns or 'high' not in df.columns or 'low' not in df.columns or 'close' not in df.columns:
        raise ValueError("CSV must contain datetime, open, high, low, close columns")
    df = df.rename(columns=rename_map)
    df = df.sort_values(by='datetime').reset_index(drop=True)
    params = default_params()
    df_full = build_features(df, params)
    preds, sigs, trades = generate_signals_and_backtest(df_full, params)
    print("Generated", len(trades), "trades")
    print(simple_metrics(trades))
    if not trades.empty:
        print(trades.head())
    # quick equity plot: cumulative pnl
    if not trades.empty:
        trades['cum'] = trades['pnl'].cumsum()
        plt.plot(trades['cum'])
        plt.title('Cumulative PnL of trades')
        plt.xlabel('trade #')
        plt.ylabel('PnL')
        plt.show()

Пояснення / обмеження та наступні кроки

  1. Я зберіг основну архітектуру: feature-engineer → Lorentzian KNN → фільтри → правила входу/виходу → SL/TP/позначки. Але через відмінності середовищ (bar-indexing у Pine та shift-и у pandas) можливі зміщення на 0–1 бар — це нормально і легко поправиться під час калібрування.
  2. Kernel regression: моя версія nadaraya_watson — спрощена, але дає ту саму ідею (Gaussian / Rational Quadratic). Можемо підмітити параметри h, r, x, lag під твої дані.
  3. Backtester: я зробив простий бар-за-баром симулятор: входи на close бару, виходи при досягненні high/low наступних барів (як у Pine). Це дає коректну оцінку для більшості випадків; якщо хочеш — можу підключити vectorbt або backtesting.py для розширеної аналітики й швидшого тестування.
  4. Дальші покращення (можу зробити далі, якщо потрібно):
    • точніша копія ml бібліотеки (jdehorty/MLExtensions) — відтворити внутрішні фічі,
    • точна візуалізація barcolors / kernel estimate поверх ціни,
    • автопідбір параметрів/оптимізація (grid search),
    • збереження результатів у CSV або графіки з matplotlib/plotly,
    • інтеграція з даними реального часу через ccxt чи yfinance.

Схожі статті

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Back to top button