Indicator-Based Trading Bot Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Indicator-Based Trading Bot Development
Medium
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Trading Bot Based on Technical Indicators

A bot based on technical indicators is the most common class of trading algorithms. The strategy codifies entry/exit rules through RSI, MACD, Bollinger Bands, volume, and other indicators. The developer's main task is to correctly implement the logic, avoid lookahead bias, and ensure reliable execution.

Typical Indicator Strategies

RSI Oversold/Overbought Strategy

import pandas_ta as ta
import pandas as pd

class RSIStrategy:
    def __init__(self, rsi_period=14, oversold=30, overbought=70):
        self.rsi_period = rsi_period
        self.oversold = oversold
        self.overbought = overbought

    def generate_signal(self, df: pd.DataFrame) -> str:
        # IMPORTANT: shift(1) — use only closed candles
        rsi = ta.rsi(df['close'], length=self.rsi_period).shift(1)
        prev_rsi = rsi.shift(1)

        current_rsi = rsi.iloc[-1]
        previous_rsi = prev_rsi.iloc[-1]

        # Entry from oversold zone (crossover from below)
        if previous_rsi < self.oversold and current_rsi >= self.oversold:
            return 'BUY'

        # Entry from overbought zone (crossover from above)
        if previous_rsi > self.overbought and current_rsi <= self.overbought:
            return 'SELL'

        return 'HOLD'

MACD Strategy

class MACDStrategy:
    def __init__(self, fast=12, slow=26, signal=9):
        self.fast = fast
        self.slow = slow
        self.signal = signal

    def generate_signal(self, df: pd.DataFrame) -> str:
        macd_data = ta.macd(df['close'], fast=self.fast, slow=self.slow, signal=self.signal)
        macd_line = macd_data[f'MACD_{self.fast}_{self.slow}_{self.signal}'].shift(1)
        signal_line = macd_data[f'MACDs_{self.fast}_{self.slow}_{self.signal}'].shift(1)

        # MACD and Signal line crossover
        crossover_up = macd_line.iloc[-2] < signal_line.iloc[-2] and macd_line.iloc[-1] >= signal_line.iloc[-1]
        crossover_down = macd_line.iloc[-2] > signal_line.iloc[-2] and macd_line.iloc[-1] <= signal_line.iloc[-1]

        # Additional filter: crossover above/below zero line
        if crossover_up and macd_line.iloc[-1] < 0:  # in negative zone — weaker
            return 'BUY' if self.config.trade_below_zero else 'HOLD'
        elif crossover_up:
            return 'BUY'
        elif crossover_down:
            return 'SELL'

        return 'HOLD'

Combined Strategy (multi-indicator confirmation)

One indicator gives many false signals. A combination of several is more accurate:

class MultiIndicatorStrategy:
    def generate_signal(self, df: pd.DataFrame) -> str:
        rsi = ta.rsi(df['close'], 14).shift(1)
        ema_20 = ta.ema(df['close'], 20).shift(1)
        ema_50 = ta.ema(df['close'], 50).shift(1)
        volume_ma = df['volume'].rolling(20).mean().shift(1)

        current_close = df['close'].iloc[-1]

        # BUY conditions: all three factors align
        trend_up = ema_20.iloc[-1] > ema_50.iloc[-1]
        rsi_ok = 40 < rsi.iloc[-1] < 65  # not overbought, but above neutral
        volume_confirm = df['volume'].iloc[-1] > volume_ma.iloc[-1] * 1.3
        price_above_ema = current_close > ema_20.iloc[-1]

        if trend_up and rsi_ok and volume_confirm and price_above_ema:
            return 'BUY'

        # SELL conditions: trend broken
        if ema_20.iloc[-1] < ema_50.iloc[-1] and rsi.iloc[-1] > 60:
            return 'SELL'

        return 'HOLD'

Data Processing and Architecture

Incremental Data Update

When working with live market, don't load full history at every candle:

class IncrementalCandleManager:
    def __init__(self, symbol: str, interval: str, history_length: int = 200):
        self.symbol = symbol
        self.interval = interval
        self.history_length = history_length
        self.df: pd.DataFrame = None

    async def initialize(self):
        """Load history on startup"""
        candles = await self.exchange.get_klines(
            self.symbol, self.interval, limit=self.history_length
        )
        self.df = self.to_dataframe(candles)

    def update(self, new_candle: dict):
        """Add new candle, remove old"""
        new_row = self.candle_to_row(new_candle)

        if new_candle['time'] == self.df.index[-1]:
            # Update current unclosed candle
            self.df.iloc[-1] = new_row
        else:
            # Add new closed candle
            self.df = pd.concat([self.df, pd.DataFrame([new_row])])
            # Keep fixed history length
            if len(self.df) > self.history_length:
                self.df = self.df.iloc[-self.history_length:]

Stop Loss and Take Profit

class PositionManager:
    async def open_with_sl_tp(
        self,
        symbol: str,
        side: str,
        amount: float,
        entry_price: float,
        sl_percent: float,
        tp_percent: float
    ):
        # Main order
        order = await self.exchange.place_order(symbol, side, amount)

        if side == 'buy':
            sl_price = entry_price * (1 - sl_percent / 100)
            tp_price = entry_price * (1 + tp_percent / 100)
        else:
            sl_price = entry_price * (1 + sl_percent / 100)
            tp_price = entry_price * (1 - tp_percent / 100)

        # OCO order for simultaneous SL and TP
        await self.exchange.place_oco_order(
            symbol=symbol,
            side='sell' if side == 'buy' else 'buy',
            quantity=amount,
            price=tp_price,            # limit (take profit)
            stop_price=sl_price,       # stop trigger
            stop_limit_price=sl_price * (0.995 if side == 'buy' else 1.005)
        )

Testing Indicator Strategy

Before running live — mandatory backtesting:

def backtest_strategy(strategy, candles_df: pd.DataFrame, initial_capital: float = 10000):
    capital = initial_capital
    position = None
    trades = []

    for i in range(50, len(candles_df)):  # start from 50 for indicator warmup
        window = candles_df.iloc[:i]
        signal = strategy.generate_signal(window)
        current_price = candles_df['close'].iloc[i]

        if signal == 'BUY' and position is None:
            size = capital * 0.95 / current_price  # 95% capital
            position = {'side': 'buy', 'price': current_price, 'size': size}
            capital -= size * current_price

        elif signal == 'SELL' and position and position['side'] == 'buy':
            pnl = (current_price - position['price']) * position['size']
            capital += position['size'] * current_price
            trades.append({'pnl': pnl, 'return_pct': pnl / (position['price'] * position['size']) * 100})
            position = None

    return {
        'final_capital': capital,
        'roi': (capital / initial_capital - 1) * 100,
        'num_trades': len(trades),
        'win_rate': sum(1 for t in trades if t['pnl'] > 0) / len(trades) * 100 if trades else 0,
    }

Key backtesting rules: execute on next candle open (not signal candle close), include fees in calculation, test on out-of-sample data. A good strategy shows positive results on OOS data too.