import numpy as np import pandas as pd from datetime import datetime, timedelta, timezone from pandas import DataFrame from typing import Optional, Union from freqtrade.strategy import ( IStrategy, Trade, Order, PairLocks, informative, BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter, timeframe_to_minutes, timeframe_to_next_date, timeframe_to_prev_date, merge_informative_pair, stoploss_from_absolute, stoploss_from_open, ) import talib.abstract as ta from technical import qtpylib class SimpleGrid(IStrategy): """ Grid Trading Bot Strategy This strategy implements a grid trading approach that: 1. Creates a grid of price levels around the current price 2. Places buy orders at lower levels and sell orders at higher levels 3. Uses market microstructure analysis to optimize grid placement 4. Adapts to market volatility for dynamic grid spacing """ INTERFACE_VERSION = 3 stoploss = -0.10 timeframe = "15m" # Grid Bot Parameters # This parameter determines the percentage distance between each grid level # For example, if grid_spacing = 0.005 (0.5%): # - If current price is $100 # - Grid levels might be: $99.50, $100, $100.50, $101, $101.50 # - Each level is 0.5% apart from the next # - 0.001 is 0.1%: Will create more frequent trades # - 0.05 is 5%: Will create less frequent trades # - 0.005 is 0.5%: Default value, balance between frequent and infrequent trades # We use this these values (0.1% & 5%) for testing purposes. These values are not optimized for trading fee. grid_spacing = DecimalParameter(0.001, 0.05, default=0.005, space="buy", optimize=True, load=True) # Minimum volume threshold for considering a market active # This parameter determines the minimum volume required to consider a market active # - 1000: Basic threshold for most pairs # - 10000: Higher threshold for more liquid pairs # - 100000: Very high threshold for major pairs # The actual value should be adjusted based on the specific pair's typical volume min_volume_threshold = IntParameter(1000, 100000, default=10000, space="buy", optimize=True, load=True) # This parameter determines the number of grid levels to create # - 3 is 3 levels: $99.50, $100, $100.50 # - 10 is 10 levels: $99.50, $100, $100.50, $101, $101.50, $102, $102.50, $103, $103.50, $104, $104.50 # - 5 is 5 levels: $99.50, $100, $100.50, $101, $101.50 # We use this these values (3 & 10) for testing purposes. These values are not optimized for trading fee. grid_levels = IntParameter(3, 15, default=7, space="buy", optimize=True, load=True) # This parameter determines if the grid spacing should be dynamic # - True: Will adjust the grid spacing based on market volatility # - False: Will use a fixed grid spacing # Good for trading in low/high volatility markets and prevent overtrading. # For stable markets, we don't need to use this. # We use this these values (True & False) for testing purposes. These values are not optimized for trading fee. use_dynamic_spacing = BooleanParameter(default=True, space="buy", optimize=True, load=True) # Market Microstructure Parameters # This is profit margin for the strategy. See https://trello.com/c/xUiUIDCd for more details. # # Example 1: Stock at $100 # High: $100.50 # Low: $99.50 # Close: $100 # Spread Ratio = ($100.50 - $99.50) / $100 # = $1 / $100 # = 0.01 (1%) # Example 2: Stock at $100 # High: $100.20 # Low: $99.80 # Close: $100 # Spread Ratio = ($100.20 - $99.80) / $100 # = $0.40 / $100 # = 0.004 (0.4%) min_spread_ratio = DecimalParameter(0.003, 0.01, default=0.005, space="buy", optimize=True, load=True) # This parameter determines the volatility threshold for the strategy. # Use bigger than 5% for high volatility markets. This will help in backtesting. volatility_threshold = DecimalParameter(0.01, 0.05, default=0.02, space="buy", optimize=True, load=True) # Grid levels need to pre-defined in the plot_config # Dynamically add in runtime won't work. plot_config = { "main_plot": { "grid_upper": {"color": "red", "type": "line"}, "grid_lower": {"color": "green", "type": "line"}, "grid_level_-7": {"color": "lightgreen", "type": "line"}, "grid_level_-6": {"color": "lightgreen", "type": "line"}, "grid_level_-5": {"color": "lightgreen", "type": "line"}, "grid_level_-4": {"color": "lightgreen", "type": "line"}, "grid_level_-3": {"color": "lightgreen", "type": "line"}, "grid_level_-2": {"color": "lightgreen", "type": "line"}, "grid_level_-1": {"color": "lightgreen", "type": "line"}, "grid_level_0": {"color": "yellow", "type": "line"}, "grid_level_1": {"color": "lightcoral", "type": "line"}, "grid_level_2": {"color": "lightcoral", "type": "line"}, "grid_level_3": {"color": "lightcoral", "type": "line"}, "grid_level_4": {"color": "lightcoral", "type": "line"}, "grid_level_5": {"color": "lightcoral", "type": "line"}, "grid_level_6": {"color": "lightcoral", "type": "line"}, "grid_level_7": {"color": "lightcoral", "type": "line"}, } } def calculate_grid_levels(self, current_price: float, volatility: float) -> tuple: """ Calculate grid levels based on current price and volatility Args: current_price: Current market price volatility: Current market volatility Returns: tuple: (grid_upper, grid_lower, spacing) """ if self.use_dynamic_spacing.value: # Adjust spacing based on volatility base_spacing = self.grid_spacing.value volatility_multiplier = 1 + (volatility / 0.01) # Scale volatility impact spacing = base_spacing * volatility_multiplier else: spacing = self.grid_spacing.value # Calculate grid boundaries num_levels = self.grid_levels.value grid_upper = current_price * (1 + spacing * num_levels) grid_lower = current_price * (1 - spacing * num_levels) return grid_upper, grid_lower, spacing def calculate_microstructure_metrics(self, dataframe: DataFrame) -> DataFrame: """ Calculate market microstructure metrics for better grid placement Args: dataframe: OHLCV data Returns: DataFrame with added microstructure metrics """ # Estimate bid-ask spread from OHLC data dataframe['spread_estimate'] = (dataframe['high'] - dataframe['low']) / dataframe['close'] dataframe['spread_ratio'] = dataframe['spread_estimate'].rolling(window=20).mean() # Calculate price volatility dataframe['returns'] = dataframe['close'].pct_change() dataframe['volatility'] = dataframe['returns'].rolling(window=20).std() # Identify volatility regime using volatility_threshold dataframe['volatility_regime'] = np.where( dataframe['volatility'] > self.volatility_threshold.value, # Use configured threshold 1, # High volatility regime 0 # Normal volatility regime ) return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Calculate and populate all indicators used in the strategy Args: dataframe: OHLCV data metadata: Additional pair metadata Returns: DataFrame with added indicators """ # Calculate market microstructure metrics dataframe = self.calculate_microstructure_metrics(dataframe) # Calculate grid levels if we have data if len(dataframe) > 0: current_price = dataframe['close'].iloc[-1] volatility = dataframe['volatility'].iloc[-1] grid_upper, grid_lower, spacing = self.calculate_grid_levels(current_price, volatility) # Add grid levels to dataframe dataframe['grid_upper'] = grid_upper dataframe['grid_lower'] = grid_lower dataframe['grid_spacing'] = spacing # Calculate and store all grid levels num_levels = self.grid_levels.value for i in range(-num_levels, num_levels + 1): if i == 0: level_price = current_price else: level_price = current_price * (1 + spacing * i) # Only store if within grid boundaries if grid_lower <= level_price <= grid_upper: dataframe[f'grid_level_{i}'] = level_price # Print grid levels for the latest candle print(f"\nGrid Levels for {metadata['pair']}:") print(f"Current Price: {current_price:.8f}") print(f"Grid Spacing: {spacing:.4f}") print(f"Grid Upper: {grid_upper:.8f}") print(f"Grid Lower: {grid_lower:.8f}") # Print all levels print("\nAll Grid Levels:") for i in range(-num_levels, num_levels + 1): if i == 0: level_price = current_price level_type = "CURRENT" else: level_price = current_price * (1 + spacing * i) level_type = "BUY" if i < 0 else "SELL" # Only print if within grid boundaries if grid_lower <= level_price <= grid_upper: print(f"Level {i}: {level_price:.8f} ({level_type})") return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Entry logic: Only enter when price touches specific grid levels """ # Initialize enter_long column dataframe['enter_long'] = 0 # Get the number of grid levels num_levels = self.grid_levels.value # Check for entry at each buy grid level (negative levels) for i in range(-num_levels, 0): # Only negative levels for buy entries grid_level_col = f'grid_level_{i}' # Check if this grid level exists in the dataframe if grid_level_col in dataframe.columns: # Entry condition: price touches the grid level # We consider it "touching" if the low of the candle is at or below the grid level # and the high of the candle is above the grid level (price crossed through it) dataframe.loc[ ( (dataframe['low'] <= dataframe[grid_level_col]) & # Price touched or went below the level (dataframe['high'] > dataframe[grid_level_col]) # Price also went above the level (crossed through) ), 'enter_long' ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Exit logic: 1. Stop loss: Exit if price goes below the lowest grid level 2. Take profit: Exit if price goes above the grid level where we entered """ # Initialize exit_long column dataframe['exit_long'] = 0 # Get the number of grid levels num_levels = self.grid_levels.value # 1. Stop Loss: Exit if price goes below the lowest grid level lowest_grid_level = f'grid_level_{-num_levels}' if lowest_grid_level in dataframe.columns: dataframe.loc[ (dataframe['close'] < dataframe[lowest_grid_level]), # Price below lowest grid level 'exit_long' ] = 1 # 2. Take Profit: Exit if price goes above the grid level where we entered # We need to track which grid level we entered at and exit when price goes above the corresponding positive level # For now, we'll implement a simplified version that exits when price goes above any positive grid level # This can be enhanced later to track the specific entry level for i in range(1, num_levels + 1): # Positive levels for take profit grid_level_col = f'grid_level_{i}' if grid_level_col in dataframe.columns: dataframe.loc[ (dataframe['close'] > dataframe[grid_level_col]), # Price above take profit level 'exit_long' ] = 1 return dataframe