freqtrade / freqtrade

Free, open source crypto trading bot
https://www.freqtrade.io
GNU General Public License v3.0
28.15k stars 6.03k forks source link

Strategy doesn't buy often in backtest, but buys as soon as possible in dry-run #3784

Closed Levos-IG1075 closed 4 years ago

Levos-IG1075 commented 4 years ago

Describe your environment

Your question

Hi,

I'm writing an issue because I can't find a solution to my problem. I've searched and couldn't find an answer. My issue is that my custom strategy doesn't buy often in backtest, but buys as soon as possible in dry-run (leading to a lot of ugly losses)

I've checked if my Strategy could see in the future and I don't think it's the case. Could anyone help?

Here's the strategy, it's basically a single neuron neural network trained using hyperopt (nothing fancy, but the code is a bit ugly sorry) (populate_sell_trend not included because it's basically the same)

from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
# --------------------------------

import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy as np  # noqa

def activate(x):
    return np.tanh(x)  # tanh

params = {
    '0-0-0-w': -0.53814,
    '0-0-bias': -0.96407,
    '1-0-0-w': -0.49249,
    '10-0-0-w': 0.08845,
    '11-0-0-w': -0.14317,
    '12-0-0-w': 0.00923,
    '13-0-0-w': 0.30464,
    '14-0-0-w': -0.35835,
    '15-0-0-w': -0.49712,
    '16-0-0-w': 0.76135,
    '17-0-0-w': -0.75257,
    '18-0-0-w': -0.04622,
    '19-0-0-w': 0.10012,
    '2-0-0-w': -0.23534,
    '20-0-0-w': -0.04553,
    '21-0-0-w': -0.35334,
    '22-0-0-w': 0.17952,
    '23-0-0-w': 0.44446,
    '24-0-0-w': -0.15875,
    '25-0-0-w': 0.97565,
    '26-0-0-w': -0.89948,
    '27-0-0-w': 0.61777,
    '28-0-0-w': -0.60204,
    '29-0-0-w': -0.85229,
    '3-0-0-w': 0.47262,
    '30-0-0-w': -0.52791,
    '31-0-0-w': 0.98494,
    '4-0-0-w': -0.54942,
    '5-0-0-w': 0.40523,
    '6-0-0-w': 0.4723,
    '7-0-0-w': 0.63297,
    '8-0-0-w': 0.07159,
    '9-0-0-w': -0.86791,
    'adx-bias': -0.48719,
    'ao-bias': -0.87518,
    'aroonosc-bias': -0.56096,
    'bb_percent-bias': -0.98703,
    'bb_width-bias': -0.73742,
    'cci-bias': 0.47039,
    'end-0-w': -0.81658,
    'end-bias': 0.74656,
    'fastd-bias': -0.2793,
    'fisher_rsi_norm-bias': -0.36065,
    'kc_percent-bias': 0.76707,
    'kc_width-bias': 0.5489,
    'macd-bias': 0.55448,
    'macdhist-bias': -0.83133,
    'macdsignal-bias': 0.30828,
    'mfi-bias': -0.13097,
    'roc-bias': -0.78885,
    'rsi-bias': 0.9856,
    'sar-bias': 0.43812,
    'sma10-bias': -0.39019,
    'sma100-bias': 0.03558,
    'sma21-bias': 0.07457,
    'sma3-bias': 0.93633,
    'sma5-bias': -0.93329,
    'sma50-bias': -0.60637,
    'tema10-bias': -0.45946,
    'tema100-bias': 0.1662,
    'tema21-bias': 0.68466,
    'tema3-bias': 0.25368,
    'tema5-bias': -0.88818,
    'tema50-bias': 0.3019,
    'uo-bias': 0.71019,
    'wbb_percent-bias': -0.55964,
    'wbb_width-bias': 0.23523,

    's-0-0-0-w': 0.85409,
    's-0-0-bias': -0.04613,
    's-1-0-0-w': -0.14997,
    's-10-0-0-w': -0.67008,
    's-11-0-0-w': -0.40221,
    's-12-0-0-w': 0.64553,
    's-13-0-0-w': 0.22838,
    's-14-0-0-w': 0.99977,
    's-15-0-0-w': 0.89363,
    's-16-0-0-w': -0.88212,
    's-17-0-0-w': -0.71813,
    's-18-0-0-w': 0.41602,
    's-19-0-0-w': -0.48389,
    's-2-0-0-w': 0.09649,
    's-20-0-0-w': 0.64273,
    's-21-0-0-w': -0.31671,
    's-22-0-0-w': 0.9663,
    's-23-0-0-w': 0.00229,
    's-24-0-0-w': 0.96244,
    's-25-0-0-w': -0.24513,
    's-26-0-0-w': 0.52312,
    's-27-0-0-w': 0.44742,
    's-28-0-0-w': -0.03916,
    's-29-0-0-w': 0.88882,
    's-3-0-0-w': -0.32112,
    's-30-0-0-w': -0.70886,
    's-31-0-0-w': -0.42672,
    's-4-0-0-w': -0.55265,
    's-5-0-0-w': 0.56105,
    's-6-0-0-w': 0.47436,
    's-7-0-0-w': 0.58136,
    's-8-0-0-w': -0.48308,
    's-9-0-0-w': -0.16024,
    's-adx-bias': -0.4091,
    's-ao-bias': 0.76889,
    's-aroonosc-bias': 0.16228,
    's-bb_percent-bias': 0.19407,
    's-bb_width-bias': 0.11795,
    's-cci-bias': 0.8379,
    's-end-0-w': -0.14648,
    's-end-bias': -0.85697,
    's-fastd-bias': -0.00581,
    's-fisher_rsi_norm-bias': -0.05253,
    's-kc_percent-bias': -0.3562,
    's-kc_width-bias': 0.67451,
    's-macd-bias': -0.17742,
    's-macdhist-bias': -0.58328,
    's-macdsignal-bias': -0.79847,
    's-mfi-bias': -0.48236,
    's-roc-bias': -0.5914,
    's-rsi-bias': -0.9618,
    's-sar-bias': 0.57033,
    's-sma10-bias': 0.14349,
    's-sma100-bias': 0.02401,
    's-sma21-bias': 0.78191,
    's-sma3-bias': 0.72279,
    's-sma5-bias': -0.19383,
    's-sma50-bias': 0.63697,
    's-tema10-bias': 0.96837,
    's-tema100-bias': 0.77171,
    's-tema21-bias': 0.67279,
    's-tema3-bias': -0.24583,
    's-tema5-bias': -0.08997,
    's-tema50-bias': 0.65532,
    's-uo-bias': 0.67701,
    's-wbb_percent-bias': -0.658,
    's-wbb_width-bias': -0.71056
}

network_shape = [1]

class MyStrategy(IStrategy):
    # ROI table:
    minimal_roi = {
        "0": 0.21029,
        "11": 0.05876,
        "57": 0.02191,
        "281": 0
    }

    # Stoploss:
    stoploss = -0.07693

    # Optimal ticker interval for the strategy
    ticker_interval = '2h'

    # Trailing stop:
    trailing_only_offset_is_reached = False
    trailing_stop = True
    trailing_stop_positive = 0.01019
    trailing_stop_positive_offset = 0.01164

    # run "populate_indicators" only for new candle
    process_only_new_candles = True

    # Experimental settings (configuration will overide these if set)
    use_sell_signal = True
    sell_profit_only = True
    ignore_roi_if_buy_signal = True

    startup_candle_count = 100

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Adds several different TA indicators to the given DataFrame

        Performance Note: For the best performance be frugal on the number of indicators
        you are using. Let uncomment only the indicator you are using in your strategies
        or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
        """

        # Momentum Indicators
        # ------------------------------------

        # ADX
        dataframe['adx'] = ta.ADX(dataframe) / 100

        # # Plus Directional Indicator / Movement
        # dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
        # dataframe['plus_di'] = ta.PLUS_DI(dataframe)

        # # Minus Directional Indicator / Movement
        # dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
        # dataframe['minus_di'] = ta.MINUS_DI(dataframe)

        # # Aroon, Aroon Oscillator
        # aroon = ta.AROON(dataframe)
        # dataframe['aroonup'] = aroon['aroonup']
        # dataframe['aroondown'] = aroon['aroondown']
        dataframe['aroonosc'] = ta.AROONOSC(dataframe) / 100

        # # Awesome Oscillator
        dataframe['ao'] = ((qtpylib.awesome_oscillator(dataframe) > 0).astype(int) - 0.5) * 2

        # # Keltner Channel
        keltner = qtpylib.keltner_channel(dataframe)
        dataframe["kc_upperband"] = keltner["upper"]
        dataframe["kc_lowerband"] = keltner["lower"]
        dataframe["kc_middleband"] = keltner["mid"]
        dataframe["kc_percent"] = (
                (dataframe["close"] - dataframe["kc_lowerband"]) /
                (dataframe["kc_upperband"] - dataframe["kc_lowerband"])
        )
        dataframe["kc_width"] = (
                (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"]
        )

        # # Ultimate Oscillator
        dataframe['uo'] = ta.ULTOSC(dataframe) / 100

        # # Commodity Channel Index: values [Oversold:-100, Overbought:100]
        dataframe['cci'] = ta.CCI(dataframe) / 200

        # RSI
        dataframe['rsi'] = ta.RSI(dataframe) / 100

        # # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy)
        rsi = 0.1 * (dataframe['rsi'] * 100 - 50)
        fisher_rsi = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)

        # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy)
        dataframe['fisher_rsi_norm'] = 50 * (fisher_rsi + 1) / 100

        # # Stochastic Slow
        # stoch = ta.STOCH(dataframe)
        # dataframe['slowd'] = stoch['slowd']
        # dataframe['slowk'] = stoch['slowk']

        # Stochastic Fast
        stoch_fast = ta.STOCHF(dataframe)
        dataframe['fastd'] = stoch_fast['fastd'] / 100

        # # Stochastic RSI
        # stoch_rsi = ta.STOCHRSI(dataframe)
        # dataframe['fastd_rsi'] = stoch_rsi['fastd']
        # dataframe['fastk_rsi'] = stoch_rsi['fastk']

        # MACD
        macd = ta.MACD(dataframe)
        dataframe['macd'] = macd['macd']
        dataframe['macdsignal'] = macd['macdsignal']
        dataframe['macdhist'] = macd['macdhist']

        # MFI
        dataframe['mfi'] = ta.MFI(dataframe) / 100

        # # ROC
        dataframe['roc'] = ta.ROC(dataframe) / 100

        # Overlap Studies
        # ------------------------------------

        # Bollinger Bands
        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
        dataframe['bb_lowerband'] = bollinger['lower']
        dataframe['bb_middleband'] = bollinger['mid']
        dataframe['bb_upperband'] = bollinger['upper']
        dataframe["bb_percent"] = (
                (dataframe["close"] - dataframe["bb_lowerband"]) /
                (dataframe["bb_upperband"] - dataframe["bb_lowerband"])
        )
        dataframe["bb_width"] = (
                (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"]
        )

        # Bollinger Bands - Weighted (EMA based instead of SMA)
        weighted_bollinger = qtpylib.weighted_bollinger_bands(
            qtpylib.typical_price(dataframe), window=20, stds=2
        )
        dataframe["wbb_upperband"] = weighted_bollinger["upper"]
        dataframe["wbb_lowerband"] = weighted_bollinger["lower"]
        dataframe["wbb_middleband"] = weighted_bollinger["mid"]
        dataframe["wbb_percent"] = (
                (dataframe["close"] - dataframe["wbb_lowerband"]) /
                (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"])
        )
        dataframe["wbb_width"] = (
                (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) / dataframe["wbb_middleband"]
        )

        # # SMA - Simple Moving Average
        dataframe['sma3'] = dataframe['close'] / ta.SMA(dataframe, timeperiod=3) - 1
        dataframe['sma5'] = dataframe['close'] / ta.SMA(dataframe, timeperiod=5) - 1
        dataframe['sma10'] = dataframe['close'] / ta.SMA(dataframe, timeperiod=10) - 1
        dataframe['sma21'] = dataframe['close'] / ta.SMA(dataframe, timeperiod=21) - 1
        dataframe['sma50'] = dataframe['close'] / ta.SMA(dataframe, timeperiod=50) - 1
        dataframe['sma100'] = dataframe['close'] / ta.SMA(dataframe, timeperiod=100) - 1

        # Parabolic SAR
        dataframe['sar'] = dataframe['close'] / ta.SAR(dataframe) - 1

        # TEMA - Triple Exponential Moving Average
        dataframe['tema3'] = dataframe['close'] / ta.TEMA(dataframe, timeperiod=3) - 1
        dataframe['tema5'] = dataframe['close'] / ta.TEMA(dataframe, timeperiod=5) - 1
        dataframe['tema10'] = dataframe['close'] / ta.TEMA(dataframe, timeperiod=10) - 1
        dataframe['tema21'] = dataframe['close'] / ta.TEMA(dataframe, timeperiod=21) - 1
        dataframe['tema50'] = dataframe['close'] / ta.TEMA(dataframe, timeperiod=50) - 1
        dataframe['tema100'] = dataframe['close'] / ta.TEMA(dataframe, timeperiod=100) - 1

        return dataframe

    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Based on TA indicators, populates the buy signal for the given dataframe
        :param dataframe: DataFrame
        :return: DataFrame with buy column
        """
        indicators = ['aroonosc', 'ao', 'uo', 'cci', 'rsi', 'fisher_rsi_norm', 'sar',
                      'sma3', 'sma5', 'sma10', 'sma21', 'sma50', 'sma100',
                      'tema3', 'tema5', 'tema10', 'tema21', 'tema50', 'tema100',
                      'fastd', 'adx', 'bb_percent', 'bb_width', 'macd', 'macdsignal', 'macdhist', 'mfi',
                      'wbb_percent', 'wbb_width', 'roc', 'kc_percent', 'kc_width']
        inputs = []
        for indicator in indicators:
            inputs.append(dataframe[indicator] + params[indicator + '-bias'])

        for index, layer_size in enumerate(network_shape):
            outputs = []
            for n in range(layer_size):
                weight = 0
                for i, input in enumerate(inputs):
                    weight += params['{}-{}-{}-w'.format(i, index, n)] * input
                weight += params['{}-{}-bias'.format(index, n)]
                outputs.append(activate(weight))
            inputs = outputs

        weight = 0
        for i, input in enumerate(inputs):
            weight += params['end-{}-w'.format(i)] * input
        weight += params['end-bias']

        dataframe.loc[activate(weight) > 0, 'buy'] = 1

        # Check that the candle had volume
        dataframe.loc[dataframe['volume'] <= 0, 'buy'] = 0

        return dataframe

Also, here's my config.json:

{
    "max_open_trades": 30,
    "stake_currency": "ETH",
    "stake_amount": 10,
    "tradable_balance_ratio": 0.99,
    "fiat_display_currency": "USD",
    "timeframe": "2h",
    "dry_run": true,
    "dry_run_wallet": 1,
    "cancel_open_orders_on_exit": false,
    "unfilledtimeout": {
        "buy": 3,
        "sell": 15
    },
    "order_time_in_force": {
        "buy": "gtc",
        "sell": "gtc"
    },
    "order_types": {
        "buy": "limit",
        "sell": "market",
        "emergencysell": "market",
        "stoploss": "market",
        "stoploss_on_exchange": true,
        "stoploss_on_exchange_interval": 60,
        "stoploss_on_exchange_limit_ratio": 0.99
    },
    "bid_strategy": {
        "price_side": "ask",
        "ask_last_balance": 0.0,
        "use_order_book": false,
        "order_book_top": 1,
        "check_depth_of_market": {
            "enabled": false,
            "bids_to_ask_delta": 1
        }
    },
    "ask_strategy": {
        "price_side": "bid",
        "use_order_book": false,
        "order_book_min": 1,
        "order_book_max": 1,
        "use_sell_signal": true,
        "sell_profit_only": false,
        "ignore_roi_if_buy_signal": true
    },
    "exchange": {
        "name": "binance",
        "key": "",
        "secret": "",
        "ccxt_config": {"enableRateLimit": true},
        "ccxt_async_config": {
            "enableRateLimit": true,
            "rateLimit": 200
        },
        "pair_whitelist": [
            "ADA/ETH","ADX/ETH","AE/ETH","AION/ETH","BAT/ETH","BLZ/ETH","BNT/ETH","BQX/ETH","BRD/ETH","CDT/ETH","CMT/ETH","CVC/ETH","DASH/ETH","DATA/ETH","DENT/ETH","ELF/ETH","ENG/ETH","ENJ/ETH","EOS/ETH","ETC/ETH","EVX/ETH","FUN/ETH","GNT/ETH","GXS/ETH","HOT/ETH","ICX/ETH","IOST/ETH","IOTA/ETH","IOTX/ETH","KEY/ETH","KMD/ETH","KNC/ETH","LEND/ETH","LINK/ETH","LOOM/ETH","LRC/ETH","LSK/ETH","LTC/ETH","MANA/ETH","MCO/ETH","MFT/ETH","MTL/ETH","NANO/ETH","NAS/ETH","NCASH/ETH","NEBL/ETH","NEO/ETH","NPXS/ETH","NULS/ETH","OMG/ETH","ONT/ETH","OST/ETH","PIVX/ETH","POWR/ETH","QKC/ETH","QLC/ETH","QSP/ETH","QTUM/ETH","REP/ETH","RLC/ETH","SC/ETH","SNT/ETH","STEEM/ETH","STMX/ETH","STORJ/ETH","STRAT/ETH","THETA/ETH","TNT/ETH","TRX/ETH","VET/ETH","VIB/ETH","WAN/ETH","WAVES/ETH","WTC/ETH","XEM/ETH","XLM/ETH","XMR/ETH","XRP/ETH","XVG/ETH","XZC/ETH","ZEC/ETH","ZEN/ETH","ZIL/ETH","ZRX/ETH"
        ],
        "pair_blacklist": [
            "BNB/BTC",
            "BNB/BUSD",
            "BNB/ETH",
            "BNB/EUR",
            "BNB/NGN",
            "BNB/PAX",
            "BNB/RUB",
            "BNB/TRY",
            "BNB/TUSD",
            "BNB/USDC",
            "BNB/USDS",
            "BNB/USDT",
        ]
    },
    "pairlists": [
        {"method": "StaticPairList"},
        {"method": "PriceFilter", "low_price_ratio": 0.01},
        {"method": "SpreadFilter", "max_spread_ratio": 0.0075}
    ],
    "edge": {
        "enabled": false,
        "process_throttle_secs": 3600,
        "calculate_since_number_of_days": 7,
        "allowed_risk": 0.01,
        "stoploss_range_min": -0.01,
        "stoploss_range_max": -0.1,
        "stoploss_range_step": -0.01,
        "minimum_winrate": 0.60,
        "minimum_expectancy": 0.20,
        "min_trade_number": 10,
        "max_trade_duration_minute": 1440,
        "remove_pumps": false
    },
    "telegram": {
        "enabled": false,
        "token": "",
        "chat_id": ""
    },
    "api_server": {
        "enabled": false,
        "listen_ip_address": "127.0.0.1",
        "listen_port": 8080,
        "verbosity": "info",
        "jwt_secret_key": "somethingrandom",
        "CORS_origins": [],
        "username": "",
        "password": ""
    },
    "initial_state": "running",
    "forcebuy_enable": false,
    "internals": {
        "process_throttle_secs": 5
    }
}

Any insight would be really appreciated. Thanks.

xmatthias commented 4 years ago

I'm not sure what youd expect ... there's no "obvious" lookahead / look into the future - but having few buys in backtesting, and many buys in dry-run is really the opposite signal of this - as lookahead biased strategies often have 0 buys in dry-run.

I think the main key is how you did get the weights / train the model. I don't think you can use Hyperopt for this (without some magic in your methods) - as hyperopt basically just runs backtesting over and over - always over the complete dataset. The methods (populate_buy / populate_sell / populate_indicators) are called for the whole dataset - so the strategy needs to make sure to not look into the future.

Depending on how you did the training, then the model will just look at the whole data and pick favorable points (however knowing how the future evolved).

What you'll probably need to do to properly train a model (depends on how you implement it ...)

iterate in 1unit steps through the whole data with a data-size of a few 100 (i use 300 below - assume 500 as maximum as freqtrade does not really give you more data in dry / live modes), so for example:

first step: data from index 0:300 2nd step: data from index 1:301 3rd step: data from index 2:302 ...

Neither hyperopt nor Backtesting will do this (as it would take a VERY long time).

Usually (with sklearn models) - you'd flatten the data - so all data for the first step is flattened into one very wide row, so you'd have close, close_1, close_2 ... - allowing the model to look at the last 300 rows.

Obviously you'll then have 300 fewer rows in the output.

Now flattening the rows may or may not be necessary, depending on the model ... especially if you implement the model yourself from scratch, this can probably be accounted for.

It's just a guess as to why your model underperforms - but i hope it gives you some ideas.

Levos-IG1075 commented 4 years ago

Hi @xmatthias, thanks for the reply!

I'm not sure what youd expect ...

Over the same time period in backtest and dry-run, I'd expect the same buy/sells to occur. Maybe I misunderstood something?

I don't think you can use Hyperopt for this

It's basically what I did: I have a script that take a network shape and generate an Hyperopt class file from it. It's very ugly and, as you expected, very slow. It can barely train a neural network with up to 10 neurons realistically.

In the above code, it's a single-neuron network. The weights are coming directly from Hyperopt :)

xmatthias commented 4 years ago

I'm not sure what youd expect ...

Over the same time period in backtest and dry-run, I'd expect the same buy/sells to occur. Maybe I misunderstood something?

That was clear - i mean from this issue.

I don't think it's something obvious in the code you shared (doesn't mean it can't be some subtle bug) - which means it's probably something that's wrong with how the training of the network is done (or the bot opens the 30 positions in backtesting, and then keeps them open for a very long time - which would result in very few trades).

I hope you understand that we'll not spend hours trying to find what's wrong with your code.

Levos-IG1075 commented 4 years ago

Yes don't worry I know you won't help me regarding the Strategy code, it's fine.

What I found strange is that I got so different results. A backtest from September 1 didn't open any trades, whereas a dry run from the same time bought at least 30 times.

Maybe there is a bug regarding how the new candles are handled? (I could see the strategy having issues with partial candles, especially with 2h candles)

Sorry if there's any confusion, English is not my native language and I don't want to bother you.

-------- Message d'origine -------- Le 18 sept. 2020 à 06:07, Matthias a écrit :

I'm not sure what youd expect ...

Over the same time period in backtest and dry-run, I'd expect the same buy/sells to occur. Maybe I misunderstood something?

That was clear - i mean from this issue.

I don't think it's something obvious in the code you shared (doesn't mean it can't be some subtle bug) - which means it's probably something that's wrong with how the training of the network is done (or the bot opens the 30 positions in backtesting, and then keeps them open for a very long time - which would result in very few trades).

I hope you understand that we'll not spend hours trying to find what's wrong with your code.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or unsubscribe.

xmatthias commented 4 years ago

The key during dry/live operations is the last row of the dataframe. If the buy-signal is active there - then the bot will buy. All other rows are only used to generate this signal, but are irrelevant for the bot in this operation mode.

In backtesting, all rows of the datafarame are considered.

you may want to try to analyze the strategy interactively in a jupyter notebook ...

Best test how the strategy behaves if you only give it 499 candles (and look at the last few row). You can then shift the data and see how it would generate signals in other cases. You can also try to give it a longer period (the timerange you used for backtesting) - maybe you can figure out the difference wia that mode.

Levos-IG1075 commented 4 years ago

Thank you, I'll try to figure it out in the next days.

If anything comes out, I'll post my solution here.

epetros commented 2 years ago

Thank you, I'll try to figure it out in the next days.

If anything comes out, I'll post my solution here.

Hello. Interesting approach, did you manage to get it to work?