diff --git a/quants_lab/controllers/supertrend.py b/quants_lab/controllers/supertrend.py new file mode 100644 index 00000000..6da96c44 --- /dev/null +++ b/quants_lab/controllers/supertrend.py @@ -0,0 +1,52 @@ +import time + +import pandas as pd +from pydantic import Field + +from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor +from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel +from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_controller_base import ( + DirectionalTradingControllerBase, + DirectionalTradingControllerConfigBase, +) + + +class SuperTrendConfig(DirectionalTradingControllerConfigBase): + strategy_name: str = "supertrend" + length: int = Field(default=20, ge=5, le=200) + multiplier: float = Field(default=4.0, ge=2.0, le=7.0) + percentage_threshold: float = Field(default=0.01, ge=0.005, le=0.05) + + +class SuperTrend(DirectionalTradingControllerBase): + def __init__(self, config: SuperTrendConfig): + super().__init__(config) + self.config = config + + def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + # If an executor has an active position, should we close it based on a condition. This feature is not available + # for the backtesting yet + return False + + def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool: + # After finishing an order, the executor will be in cooldown for a certain amount of time. + # This prevents the executor from creating a new order immediately after finishing one and execute a lot + # of orders in a short period of time from the same side. + if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time(): + return True + return False + + def get_processed_data(self) -> pd.DataFrame: + df = self.candles[0].candles_df + df.ta.supertrend(length=self.config.length, multiplier=self.config.multiplier, append=True) + df["percentage_distance"] = abs(df["close"] - df[f"SUPERT_{self.config.length}_{self.config.multiplier}"]) / df["close"] + + # Generate long and short conditions + long_condition = (df[f"SUPERTd_{self.config.length}_{self.config.multiplier}"] == 1) & (df["percentage_distance"] < self.config.percentage_threshold) + short_condition = (df[f"SUPERTd_{self.config.length}_{self.config.multiplier}"] == -1) & (df["percentage_distance"] < self.config.percentage_threshold) + + # Choose side + df['signal'] = 0 + df.loc[long_condition, 'signal'] = 1 + df.loc[short_condition, 'signal'] = -1 + return df diff --git a/quants_lab/research_notebooks/01_strategy_design_supertrend.ipynb b/quants_lab/research_notebooks/01_strategy_design_supertrend.ipynb new file mode 100644 index 00000000..c5f7b906 --- /dev/null +++ b/quants_lab/research_notebooks/01_strategy_design_supertrend.ipynb @@ -0,0 +1,215 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# RESEARCH NOTEBOOK --> SUPERTREND" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import pandas_ta as ta # noqa: F401\n", + "\n", + "candles = pd.read_csv(\n", + " \"/Users/dardonacci/Documents/work/dashboard/data/candles/candles_binance_perpetual_WLD-USDT_3m.csv\")" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "candles.head()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "super_trend_lenght = 20\n", + "super_trend_multiplier = 3\n", + "candles.ta.supertrend(length=super_trend_lenght, multiplier=super_trend_multiplier)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "candles.ta.supertrend(length=super_trend_lenght, multiplier=super_trend_multiplier, append=True)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "candles[\"date\"] = pd.to_datetime(candles[\"timestamp\"], unit='ms')\n", + "candles.head()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import plotly.graph_objects as go\n", + "\n", + "# We are going to use just a subset to see the graph better\n", + "candles = candles.tail(800)\n", + "\n", + "# Create a candlestick chart\n", + "fig = go.Figure(data=[go.Candlestick(\n", + " x=candles['date'],\n", + " open=candles['open'],\n", + " high=candles['high'],\n", + " low=candles['low'],\n", + " close=candles['close'])\n", + "])\n", + "fig.show()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "super_trend_long = candles[candles[\"SUPERTd_20_3.0\"] == 1]\n", + "super_trend_short = candles[candles[\"SUPERTd_20_3.0\"] == -1]\n", + "# Add the SuperTrend line\n", + "fig.add_trace(go.Scatter(x=super_trend_long['date'], y=super_trend_long['SUPERT_20_3.0'],\n", + " mode='markers',\n", + " name='SuperTrend Long',\n", + " line=dict(color=\"green\"),\n", + " ))\n", + "# Add the SuperTrend line\n", + "fig.add_trace(go.Scatter(x=super_trend_short['date'], y=super_trend_short['SUPERT_20_3.0'],\n", + " mode='markers',\n", + " name='SuperTrend Short',\n", + " line=dict(color=\"red\")))" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "percentage_threshold = 0.01 # This is an example threshold value\n", + "\n", + "candles[\"percentage_distance\"] = abs(candles[\"close\"] - candles[\"SUPERT_20_3.0\"]) / candles[\"close\"]\n", + "\n", + "candles[\"signal\"] = 0\n", + "long_condition = (candles[\"SUPERTd_20_3.0\"] == 1) & (candles[\"percentage_distance\"] < percentage_threshold)\n", + "short_condition = (candles[\"SUPERTd_20_3.0\"] == -1) & (candles[\"percentage_distance\"] < percentage_threshold)\n", + "\n", + "candles.loc[long_condition, \"signal\"] = 1\n", + "candles.loc[short_condition, \"signal\"] = -1" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "from plotly.subplots import make_subplots\n", + "\n", + "fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.02, subplot_titles=('OHLC', 'Signal'),\n", + " row_heights=[0.7, 0.3])\n", + "\n", + "# Add candlestick\n", + "fig.add_trace(go.Candlestick(\n", + " x=candles['date'],\n", + " open=candles['open'],\n", + " high=candles['high'],\n", + " low=candles['low'],\n", + " close=candles['close']),\n", + " row=1, col=1)\n", + "\n", + "# Add the SuperTrend line\n", + "fig.add_trace(go.Scatter(x=super_trend_long['date'], y=super_trend_long['SUPERT_20_3.0'],\n", + " mode='markers',\n", + " name='SuperTrend Long',\n", + " line=dict(color=\"green\")),\n", + " row=1, col=1)\n", + "# Add the SuperTrend line\n", + "fig.add_trace(go.Scatter(x=super_trend_short['date'], y=super_trend_short['SUPERT_20_3.0'],\n", + " mode='markers',\n", + " name='SuperTrend Short',\n", + " line=dict(color=\"red\")),\n", + " row=1, col=1)\n", + "\n", + "# Add the signal line\n", + "fig.add_trace(go.Scatter(x=candles['date'], y=candles['signal'],\n", + " mode='lines',\n", + " name='SuperTrend',\n", + " line=dict(color=\"white\")),\n", + " row=2, col=1)\n", + "\n", + "# Update x-axis and grid properties\n", + "fig.update_xaxes(showline=True, linewidth=2, linecolor='grey', gridcolor='lightgrey')\n", + "fig.update_yaxes(showline=True, linewidth=2, linecolor='grey', gridcolor='lightgrey')\n", + "\n", + "# Update layout to adjust the size and title\n", + "fig.update_layout(height=800, title_text=\"OHLC Chart with SuperTrend and Signals\",\n", + " yaxis_title='Price',\n", + " xaxis_rangeslider_visible=False)\n" + ], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/quants_lab/research_notebooks/02_single_controller_backtest.ipynb b/quants_lab/research_notebooks/02_single_controller_backtest.ipynb new file mode 100644 index 00000000..95a49ef3 --- /dev/null +++ b/quants_lab/research_notebooks/02_single_controller_backtest.ipynb @@ -0,0 +1,591 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "root_path = os.path.abspath(os.path.join(os.getcwd(), '../..'))\n", + "sys.path.append(root_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "from decimal import Decimal\n", + "\n", + "# Market configuration\n", + "exchange = \"binance_perpetual\"\n", + "trading_pair = \"WLD-USDT\"\n", + "interval = \"3m\"\n", + "\n", + "# Account configuration\n", + "initial_portfolio_usd = 1000\n", + "order_amount = Decimal(\"25\")\n", + "n_levels = 1\n", + "leverage = 20\n", + "trade_cost = 0.0006\n", + "\n", + "# Backtest period\n", + "start = \"2023-01-01\"\n", + "end = \"2024-01-02\"\n", + "\n", + "# Triple barrier configuration\n", + "stop_loss = Decimal(\"0.015\")\n", + "take_profit = Decimal(\"0.03\")\n", + "time_limit = 60 * 60 * 12 # 12 hours\n", + "trailing_stop_activation_price_delta = Decimal(\"0.008\")\n", + "trailing_stop_trailing_delta = Decimal(\"0.004\")" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from hummingbot.smart_components.utils.order_level_builder import OrderLevelBuilder\n", + "from hummingbot.smart_components.strategy_frameworks.data_types import (\n", + " TripleBarrierConf\n", + ")\n", + "\n", + "# Building the order levels\n", + "order_level_builder = OrderLevelBuilder(n_levels=n_levels)\n", + "order_levels = order_level_builder.build_order_levels(\n", + " amounts=order_amount,\n", + " spreads=Decimal(\"0\"),\n", + " # for directional strategies we don't need spreads since we are going to use market orders to enter\n", + " triple_barrier_confs=TripleBarrierConf(\n", + " stop_loss=stop_loss, take_profit=take_profit, time_limit=time_limit,\n", + " trailing_stop_activation_price_delta=trailing_stop_activation_price_delta,\n", + " trailing_stop_trailing_delta=trailing_stop_trailing_delta),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Let's inpect the order levels\n", + "order_levels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import sys\n", + "from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig\n", + "from quants_lab.controllers.supertrend import SuperTrend, SuperTrendConfig\n", + "\n", + "# Controller configuration\n", + "length = 100\n", + "multiplier = 3.0\n", + "percentage_threshold = 0.01\n", + "\n", + "# Creating the instance of the configuration and the controller\n", + "config = SuperTrendConfig(\n", + " exchange=exchange,\n", + " trading_pair=trading_pair,\n", + " order_levels=order_levels,\n", + " candles_config=[\n", + " CandlesConfig(connector=exchange, trading_pair=trading_pair, interval=interval, max_records=sys.maxsize),\n", + " ],\n", + " leverage=leverage,\n", + " length=length,\n", + " multiplier=multiplier,\n", + " percentage_threshold=percentage_threshold,\n", + ")\n", + "controller = SuperTrend(config=config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "from quants_lab.strategy.strategy_analysis import StrategyAnalysis\n", + "\n", + "from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_backtesting_engine import \\\n", + " DirectionalTradingBacktestingEngine\n", + "\n", + "# Creating the backtesting engine and loading the historical data\n", + "engine = DirectionalTradingBacktestingEngine(controller=controller)\n", + "engine.load_controller_data(\"../../data/candles\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Let's see what is inside the candles of the controller\n", + "engine.controller.candles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "engine.controller.candles[0].candles_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Let's understand what is inside the processed data since this is what we are going to use when generating the signal ;)\n", + "engine.controller.get_processed_data()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Let's run the backtesting\n", + "\n", + "backtesting_results = engine.run_backtesting(initial_portfolio_usd=initial_portfolio_usd,\n", + " trade_cost=trade_cost,\n", + " start=start, end=end)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Let's see what is inside the backtesting results\n", + "backtesting_results.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# now let's analyze each of the dataframes\n", + "\n", + "# 1. The processed data: this is the data that we are going to use to generate the signal\n", + "backtesting_results[\"processed_data\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# 2. The executors dataframe: this is the dataframe that contains the information of the orders that were executed\n", + "backtesting_results[\"executors_df\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# 3. The results dataframe: this is the dataframe that contains the information of the pnl of the strategy\n", + "backtesting_results[\"results\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Now let's analyze the results using the StrategyAnalysis class\n", + "strategy_analysis = StrategyAnalysis(\n", + " positions=backtesting_results[\"executors_df\"],\n", + " candles_df=backtesting_results[\"processed_data\"],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# let's visualize the PNL over time of the strategy\n", + "strategy_analysis.pnl_over_time()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "strategy_analysis.create_base_figure(volume=False, positions=False, trade_pnl=True)\n", + "fig = strategy_analysis.figure()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "fig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Now let's see how we can add the SuperTrend to the plot\n", + "\n", + "import plotly.graph_objects as go\n", + "\n", + "super_trend_long = strategy_analysis.candles_df[strategy_analysis.candles_df[f\"SUPERTd_{length}_{multiplier}\"] == 1]\n", + "super_trend_short = strategy_analysis.candles_df[strategy_analysis.candles_df[f\"SUPERTd_{length}_{multiplier}\"] == -1]\n", + "# Add the SuperTrend line\n", + "fig.add_trace(go.Scatter(x=super_trend_long.index, y=super_trend_long[f'SUPERT_{length}_{multiplier}'],\n", + " mode='markers',\n", + " name='SuperTrend Long',\n", + " line=dict(color=\"green\")),\n", + " row=1, col=1)\n", + "# Add the SuperTrend line\n", + "fig.add_trace(go.Scatter(x=super_trend_short.index, y=super_trend_short[f'SUPERT_{length}_{multiplier}'],\n", + " mode='markers',\n", + " name='SuperTrend Short',\n", + " line=dict(color=\"red\")),\n", + " row=1, col=1)\n", + "\n", + "fig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# To see the trades we will need to select a lower timeframe due the restrictions and speed of the plotly library\n", + "start_time = \"2023-11-03\"\n", + "end_time = \"2023-11-05\"\n", + "\n", + "processed_data_filtered = backtesting_results[\"processed_data\"][\n", + " (backtesting_results[\"processed_data\"][\"timestamp\"] >= start_time) &\n", + " (backtesting_results[\"processed_data\"][\"timestamp\"] <= end_time)\n", + "]\n", + "\n", + "executors_filtered = backtesting_results[\"executors_df\"][\n", + " (backtesting_results[\"executors_df\"][\"timestamp\"] >= start_time) &\n", + " (backtesting_results[\"executors_df\"][\"timestamp\"] <= end_time)\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "executors_filtered" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "\n", + "strategy_analysis = StrategyAnalysis(\n", + " positions=executors_filtered,\n", + " candles_df=processed_data_filtered,\n", + ")\n", + "\n", + "strategy_analysis.create_base_figure(volume=False, positions=True, trade_pnl=True)\n", + "fig = strategy_analysis.figure()\n", + "super_trend_long = strategy_analysis.candles_df[strategy_analysis.candles_df[f\"SUPERTd_{length}_{multiplier}\"] == 1]\n", + "super_trend_short = strategy_analysis.candles_df[strategy_analysis.candles_df[f\"SUPERTd_{length}_{multiplier}\"] == -1]\n", + "# Add the SuperTrend line\n", + "fig.add_trace(go.Scatter(x=super_trend_long.index, y=super_trend_long[f'SUPERT_{length}_{multiplier}'],\n", + " mode='markers',\n", + " name='SuperTrend Long',\n", + " line=dict(color=\"green\")),\n", + " row=1, col=1)\n", + "# Add the SuperTrend line\n", + "fig.add_trace(go.Scatter(x=super_trend_short.index, y=super_trend_short[f'SUPERT_{length}_{multiplier}'],\n", + " mode='markers',\n", + " name='SuperTrend Short',\n", + " line=dict(color=\"red\")),\n", + " row=1, col=1)\n", + "fig" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "### Scatter of PNL per Trade\n", + "This bar chart illustrates the PNL for each individual trade. Positive PNLs are shown in green and negative PNLs in red, providing a clear view of profitable vs. unprofitable trades.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "import plotly.express as px\n", + "\n", + "executors_df = backtesting_results[\"executors_df\"]\n", + "\n", + "fig = px.scatter(executors_df, x=\"timestamp\", y='net_pnl_quote', title='PNL per Trade',\n", + " color='profitable', color_continuous_scale=['red', 'green'])\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "### Scatter Plot of Volume vs. PNL\n", + "This scatter plot explores the relationship between the trade volume and the PNL for each trade. It can reveal if larger volumes are associated with higher profits or losses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "fig = px.scatter(executors_df, x='volume', y='net_pnl_quote', title='Trade Volume vs. PNL')\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "### Histogram of PNL Distribution\n", + "The histogram displays the distribution of PNL values across all trades. It helps in understanding the frequency and range of profit and loss outcomes.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "fig = px.histogram(executors_df, x='net_pnl_quote', title='PNL Distribution')\n", + "fig.show()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "# Conclusion\n", + "We can see that the indicator has potential to bring good signals to trade and might be interesting to see how we can design a market maker that shifts the mid price based on this indicator.\n", + "A lot of the short signals are wrong but if we zoom in into the loss signals we can see that the losses are not that big and the wins are bigger and if we had implemented the trailing stop feature probably a lot of them are going to be profits." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "# Next steps\n", + "- Filter only the loss signals and understand what you can do to prevent them\n", + "- Try different configuration values for the indicator\n", + "- Test in multiple markets, pick mature markets like BTC-USDT or ETH-USDT and also volatile markets like DOGE-USDT or SHIB-USDT" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/quants_lab/research_notebooks/03_optimization_notebook.ipynb b/quants_lab/research_notebooks/03_optimization_notebook.ipynb new file mode 100644 index 00000000..3db92b90 --- /dev/null +++ b/quants_lab/research_notebooks/03_optimization_notebook.ipynb @@ -0,0 +1,183 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "root_path = os.path.abspath(os.path.join(os.getcwd(), '../..'))\n", + "sys.path.append(root_path)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import traceback\n", + "from decimal import Decimal\n", + "import pandas_ta as ta # noqa: F401\n", + "\n", + "from hummingbot.core.data_type.common import PositionMode, TradeType, OrderType\n", + "from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig\n", + "from hummingbot.smart_components.strategy_frameworks.data_types import TripleBarrierConf, OrderLevel\n", + "from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingBacktestingEngine\n", + "from hummingbot.smart_components.utils.config_encoder_decoder import ConfigEncoderDecoder\n", + "from optuna import TrialPruned\n", + "from hummingbot.smart_components.utils.order_level_builder import OrderLevelBuilder\n", + "\n", + "from quants_lab.controllers.supertrend import SuperTrend, SuperTrendConfig\n", + "\n", + "# To run an optimization with optuna we need to define the objective function that will be executed for each trial\n", + "\n", + "def objective(trial):\n", + " try:\n", + " # Market configuration\n", + " exchange = \"binance_perpetual\"\n", + " trading_pair = \"WLD-USDT\"\n", + " interval = \"3m\"\n", + "\n", + " # Account configuration\n", + " initial_portfolio_usd = 1000\n", + " order_amount = Decimal(\"25\")\n", + " n_levels = 1\n", + " leverage = 20\n", + " trade_cost = 0.0006\n", + "\n", + " # Backtest period\n", + " start = \"2023-01-01\"\n", + " end = \"2024-01-02\"\n", + "\n", + " # Triple barrier configuration\n", + " stop_loss = trial.suggest_float('stop_loss', 0.01, 0.02, step=0.01)\n", + " take_profit = trial.suggest_float('take_profit', 0.01, 0.04, step=0.01)\n", + " time_limit = 60 * 60 * 12 # 12 hours\n", + " trailing_stop_activation_price_delta = Decimal(\"0.008\")\n", + " trailing_stop_trailing_delta = Decimal(\"0.004\")\n", + "\n", + " length = trial.suggest_int('length', 20, 200, step=20)\n", + " multiplier = trial.suggest_float('multiplier', 2.0, 6.0, step=1.0)\n", + " percentage_threshold = trial.suggest_float('percentage_threshold', 0.01, 0.03, step=0.01)\n", + "\n", + " # Building the order levels\n", + " order_level_builder = OrderLevelBuilder(n_levels=n_levels)\n", + " order_levels = order_level_builder.build_order_levels(\n", + " amounts=order_amount,\n", + " spreads=Decimal(\"0\"),\n", + " triple_barrier_confs=TripleBarrierConf(\n", + " stop_loss=stop_loss, take_profit=take_profit, time_limit=time_limit,\n", + " trailing_stop_activation_price_delta=trailing_stop_activation_price_delta,\n", + " trailing_stop_trailing_delta=trailing_stop_trailing_delta),\n", + " )\n", + " config = SuperTrendConfig(\n", + " exchange=exchange,\n", + " trading_pair=trading_pair,\n", + " strategy_name='supertrend',\n", + " candles_config=[\n", + " CandlesConfig(connector=exchange, trading_pair=trading_pair,\n", + " interval=interval, max_records=sys.maxsize)\n", + " ],\n", + " order_levels=order_levels,\n", + " leverage=leverage,\n", + " position_mode=PositionMode.HEDGE,\n", + " length=length,\n", + " multiplier=multiplier,\n", + " percentage_threshold=percentage_threshold,\n", + "\n", + " )\n", + " controller = SuperTrend(config=config)\n", + " engine = DirectionalTradingBacktestingEngine(controller=controller)\n", + " engine.load_controller_data(\"../../data/candles\")\n", + " backtesting_results = engine.run_backtesting(\n", + " initial_portfolio_usd=initial_portfolio_usd,\n", + " trade_cost=trade_cost,\n", + " start=start, end=end)\n", + "\n", + " strategy_analysis = backtesting_results[\"results\"]\n", + " encoder_decoder = ConfigEncoderDecoder(TradeType, OrderType, PositionMode)\n", + "\n", + " trial.set_user_attr(\"net_pnl_quote\", strategy_analysis[\"net_pnl_quote\"])\n", + " trial.set_user_attr(\"net_pnl_pct\", strategy_analysis[\"net_pnl\"])\n", + " trial.set_user_attr(\"max_drawdown_usd\", strategy_analysis[\"max_drawdown_usd\"])\n", + " trial.set_user_attr(\"max_drawdown_pct\", strategy_analysis[\"max_drawdown_pct\"])\n", + " trial.set_user_attr(\"sharpe_ratio\", strategy_analysis[\"sharpe_ratio\"])\n", + " trial.set_user_attr(\"accuracy\", strategy_analysis[\"accuracy\"])\n", + " trial.set_user_attr(\"total_positions\", strategy_analysis[\"total_positions\"])\n", + " trial.set_user_attr(\"profit_factor\", strategy_analysis[\"profit_factor\"])\n", + " trial.set_user_attr(\"duration_in_hours\", strategy_analysis[\"duration_minutes\"] / 60)\n", + " trial.set_user_attr(\"avg_trading_time_in_hours\", strategy_analysis[\"avg_trading_time_minutes\"] / 60)\n", + " trial.set_user_attr(\"win_signals\", strategy_analysis[\"win_signals\"])\n", + " trial.set_user_attr(\"loss_signals\", strategy_analysis[\"loss_signals\"])\n", + " trial.set_user_attr(\"config\", encoder_decoder.encode(config.dict()))\n", + " return strategy_analysis[\"net_pnl\"]\n", + " except Exception as e:\n", + " traceback.print_exc()\n", + " raise TrialPruned()\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import optuna\n", + "\n", + "# Now let's configure the parameters for the optimization\n", + "study_name = \"super_trend_optimization_1\"\n", + "storage= \"sqlite:///../../data/backtesting/backtesting_report.db\"\n", + "\n", + "study = optuna.create_study(direction=\"maximize\", study_name=study_name,\n", + " storage=storage,\n", + " load_if_exists=True # If the study already exists, we load it\n", + " )" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# Not let's run the optimization!\n", + "\n", + "n_trials = 200\n", + "study.optimize(objective, n_trials=n_trials)" + ], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/quants_lab/research_notebooks/04_analyze_optimization_results.ipynb b/quants_lab/research_notebooks/04_analyze_optimization_results.ipynb new file mode 100644 index 00000000..2a545c28 --- /dev/null +++ b/quants_lab/research_notebooks/04_analyze_optimization_results.ipynb @@ -0,0 +1,444 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "root_path = os.path.abspath(os.path.join(os.getcwd(), '../..'))\n", + "sys.path.append(root_path)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "\n", + "from utils.optuna_database_manager import OptunaDBManager\n", + "\n", + "db_root_path = \"../../data/backtesting/\"\n", + "db_name = \"backtesting_report.db\"\n", + "\n", + "optuna_db_manager = OptunaDBManager(db_name=db_name,\n", + " db_root_path=db_root_path)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "optuna_db_manager.studies" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "study_name = \"super_trend_optimization_1\"\n", + "df = optuna_db_manager.merged_df[optuna_db_manager.merged_df[\"study_name\"] == study_name]" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "df" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Bar Chart of Average Trading Time\n", + "This bar chart compares the average trading time across trials. It helps to quickly identify trials with unusually long or short trading times.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import plotly.express as px\n", + "\n", + "fig = px.bar(df, x='trial_id', y='avg_trading_time_in_hours', title='Average Trading Time per Trial')\n", + "fig.show()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Scatter Plot of Accuracy vs. Average Trading Time\n", + "This scatter plot shows the relationship between the accuracy and the average trading time of each trial. It can help to identify if there is any correlation between these two metrics.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "fig = px.scatter(df, x='avg_trading_time_in_hours', y='accuracy', title='Accuracy vs. Average Trading Time')\n", + "fig.show()\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Histogram of Total Positions\n", + "The histogram represents the distribution of total positions across all trials. This visualization is useful for understanding the general spread and most common values of positions.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "fig = px.histogram(df, x='total_positions', title='Distribution of Total Positions')\n", + "fig.show()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Pie Chart of Win Signals\n", + "This pie chart shows the proportion of win signals in each trial, providing a visual representation of the success rate distribution across trials.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "fig = px.pie(df, names='trial_id', values='win_signals', title='Proportion of Win Signals per Trial')\n", + "fig.show()\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Box Plot for Trial Value\n", + "A box plot for trial values to identify the range, median, and any potential outliers in the trial values.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "fig = px.box(df, y='value', title='Box Plot of Trial PNL')\n", + "fig.show()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Heatmap for Correlation Analysis\n", + "This heatmap illustrates the correlation between various numerical variables such as accuracy, average trading time, total positions, win signals, and value.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import plotly.express as px\n", + "\n", + "# Calculate the correlation matrix\n", + "correlation_matrix = df[['accuracy', 'avg_trading_time_in_hours', 'total_positions', 'win_signals', 'value']].corr()\n", + "\n", + "# Generate the heatmap\n", + "fig = px.imshow(correlation_matrix,\n", + " x=correlation_matrix.columns,\n", + " y=correlation_matrix.columns,\n", + " title='Correlation Heatmap',\n", + " labels=dict(x=\"Variable\", y=\"Variable\", color=\"Correlation\"),\n", + " color_continuous_scale='RdBu')\n", + "\n", + "fig.show()\n", + "\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Stacked Bar Chart for Win Signals vs Total Positions\n", + "A stacked bar chart displaying the ratio of win signals to total positions for each trial. This helps in visualizing the efficiency and effectiveness of each trial.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "df['loss_signals'] = df['total_positions'] - df['win_signals']\n", + "fig = px.bar(df, x='trial_id', y=['win_signals', 'loss_signals'], title='Win vs Loss Signals per Trial', labels={'value':'Number of Signals'}, hover_data=['total_positions'])\n", + "fig.show()\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Scatter Plots Against PNL\n", + "These scatter plots show how various metrics behave in relation to the PNL (Profit and Loss). This analysis can help in understanding which factors have a stronger relationship with financial outcomes.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# Scatter Plot for Accuracy vs PNL\n", + "fig_accuracy_pnl = px.scatter(df, x='accuracy', y='value', title='Accuracy vs PNL')\n", + "fig_accuracy_pnl.show()\n", + "\n", + "# Scatter Plot for Average Trading Time vs PNL\n", + "fig_tradingtime_pnl = px.scatter(df, x='avg_trading_time_in_hours', y='value', title='Average Trading Time vs PNL')\n", + "fig_tradingtime_pnl.show()\n", + "\n", + "# Scatter Plot for Total Positions vs PNL\n", + "fig_positions_pnl = px.scatter(df, x='total_positions', y='value', title='Total Positions vs PNL')\n", + "fig_positions_pnl.show()\n", + "\n", + "# Scatter Plot for Win Signals vs PNL\n", + "fig_winsignals_pnl = px.scatter(df, x='win_signals', y='value', title='Win Signals vs PNL')\n", + "fig_winsignals_pnl.show()\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "### Scatter Matrix of All Variables\n", + "A scatter matrix allows us to see both the distribution of single variables and the relationships between two variables. The diagonal shows histograms for each variable, and the scatter plots show correlations between them.\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import plotly.express as px\n", + "\n", + "# Selecting columns for scatter matrix\n", + "selected_columns = ['accuracy', 'avg_trading_time_in_hours', 'total_positions', 'win_signals', 'value']\n", + "\n", + "# Creating scatter matrix\n", + "fig_matrix = px.scatter_matrix(df[selected_columns], dimensions=selected_columns, title='Scatter Matrix of Variables', height=800)\n", + "fig_matrix.show()\n" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import json\n", + "from hummingbot.core.data_type.common import PositionMode\n", + "from hummingbot.connector.connector_base import TradeType, OrderType\n", + "from hummingbot.smart_components.utils.config_encoder_decoder import ConfigEncoderDecoder\n", + "\n", + "trial_to_analyze = 36\n", + "\n", + "trial = df[df[\"trial_id\"] == trial_to_analyze]" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "trial" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# Transform trial config in a dictionary\n", + "encoder_decoder = ConfigEncoderDecoder(TradeType, OrderType, PositionMode)\n", + "trial_config = encoder_decoder.decode(json.loads(trial[\"config\"].item()))\n", + "trial_config" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "from quants_lab.controllers.supertrend import SuperTrend, SuperTrendConfig\n", + "\n", + "# In this case we are using the supertrend controller but we can also access to the controller by using the method load_controllers\n", + "config = SuperTrendConfig(**trial_config)\n", + "controller = SuperTrend(config)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "from quants_lab.strategy.strategy_analysis import StrategyAnalysis\n", + "from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingBacktestingEngine\n", + "\n", + "# Backtest configuration\n", + "trade_cost = 0.0006\n", + "initial_portfolio_usd = 1000\n", + "start = \"2023-01-01\"\n", + "end = \"2024-01-02\"\n", + "\n", + "# Load the data\n", + "engine = DirectionalTradingBacktestingEngine(controller=controller)\n", + "engine.load_controller_data(\"../../data/candles\")\n", + "backtesting_results = engine.run_backtesting(initial_portfolio_usd=initial_portfolio_usd,\n", + " trade_cost=trade_cost,\n", + " start=start, end=end)\n", + "strategy_analysis = StrategyAnalysis(\n", + " positions=backtesting_results[\"executors_df\"],\n", + " candles_df=backtesting_results[\"processed_data\"],\n", + ")" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "strategy_analysis.create_base_figure(volume=False, positions=False, trade_pnl=True)\n", + "strategy_analysis.figure()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "strategy_analysis.pnl_over_time()" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/quants_lab/strategy/strategy_analysis.py b/quants_lab/strategy/strategy_analysis.py index be570805..c95c8d8e 100644 --- a/quants_lab/strategy/strategy_analysis.py +++ b/quants_lab/strategy/strategy_analysis.py @@ -17,10 +17,10 @@ def __init__(self, positions: pd.DataFrame, candles_df: Optional[pd.DataFrame] = self.base_figure = None def create_base_figure(self, candlestick=True, volume=True, positions=False, trade_pnl=False, extra_rows=0): - rows, heights = self.get_n_rows_and_heights(extra_rows + volume + trade_pnl, volume) + rows, heights = self.get_n_rows_and_heights(extra_rows + trade_pnl, volume) self.rows = rows specs = [[{"secondary_y": True}]] * rows - self.base_figure = make_subplots(rows=rows, cols=1, shared_xaxes=True, vertical_spacing=0.05, + self.base_figure = make_subplots(rows=rows, cols=1, shared_xaxes=True, vertical_spacing=0.01, row_heights=heights, specs=specs) if candlestick: self.add_candles_graph() @@ -29,7 +29,7 @@ def create_base_figure(self, candlestick=True, volume=True, positions=False, tra if positions: self.add_positions() if trade_pnl: - self.add_trade_pnl() + self.add_trade_pnl(row=rows) self.update_layout(volume) def add_positions(self): diff --git a/utils/optuna_database_manager.py b/utils/optuna_database_manager.py index abd23186..a1241533 100644 --- a/utils/optuna_database_manager.py +++ b/utils/optuna_database_manager.py @@ -1,5 +1,6 @@ import os import json +from typing import Optional import pandas as pd from sqlalchemy import create_engine, text @@ -9,9 +10,10 @@ class OptunaDBManager: - def __init__(self, db_name): + def __init__(self, db_name, db_root_path: Optional[str]): + db_root_path = db_root_path or "data/backtesting" self.db_name = db_name - self.db_path = f'sqlite:///{os.path.join("data/backtesting", db_name)}' + self.db_path = f'sqlite:///{os.path.join(db_root_path, db_name)}' self.engine = create_engine(self.db_path, connect_args={'check_same_thread': False}) self.session_maker = sessionmaker(bind=self.engine)