Exploring the Power of Detached Technical Indicators#

Welcome to this interactive notebook! Our primary goal here is to delve into the concept and application of detached technical indicators. These unique tools are beneficial when we’re looking to maintain a clear and uncluttered price chart, or when the indicator operates on a scale vastly different from that of the price chart. By detaching these indicators, we can analyze them separately, enhancing our ability to interpret the data they provide without compromising the readability of our main price chart.

While we’ll also be exploring a specific trading strategy within this notebook, we highly encourage you to focus on the visual output of the charts. This will enable you to better understand how detached technical indicators function in real-time market analysis. Through this visualization, you’ll appreciate the power of these indicators, and how they can improve your ability to make informed trading decisions. Remember, in trading, clarity and understanding are key, and that’s what this notebook aims to provide.

import psutil

mem = psutil.virtual_memory()
print(f"Current memory: {mem.available / 2**20:.2f} MB / {mem.total / 2**20:.2f} MB")
Current memory: 27494.54 MB / 39915.50 MB

Single trading pair EMA crossover strategy

This is an example notebook how to create and run backtests with tradeexecutor framework.

Some highlights of this notebook:

  • The backtest has all its code within a single Jupyter notebook

    • The backtest code and charts are self-contained in a single file

    • The example code is easy to read

    • Easy to test different functionalities of tradeexecutor library

  • Runs a backtest for EMA strategy on a single trading pair

    • Uses PancakeSwap on BSC chain for trading

    • Based on fast EMA and slow EMA

    • Depending on the moving average overlap, enters in to a position

  • You need a Trading Strategy API key to run the notebook

  • This backtest is made to demostrate the features

    • The strategy may or may not generate any profits, as it is not the purpose of this example

Set up#

Set up the parameters used in in this strategy backtest study.

  • Backtested blockchain, exchange and trading pair

  • Backtesting period

  • Strategy parameters for EMA crossovers

import datetime
import pandas as pd

from tradingstrategy.chain import ChainId
from tradingstrategy.timebucket import TimeBucket
from tradeexecutor.strategy.cycle import CycleDuration
from tradeexecutor.strategy.strategy_module import StrategyType, TradeRouting, ReserveCurrency

# Tell what trade execution engine version this strategy needs to use
# NOTE: this setting has currently no effect

# What kind of strategy we are running.
# This tells we are going to use
# NOTE: this setting has currently no effect
TRADING_STRATEGY_TYPE = StrategyType.managed_positions

# How our trades are routed.
# PancakeSwap basic routing supports two way trades with BUSD
# and three way trades with BUSD-BNB hop.
TRADE_ROUTING = TradeRouting.pancakeswap_busd

# How often the strategy performs the decide_trades cycle.
# We do it for every 4h.
TRADING_STRATEGY_CYCLE = CycleDuration.cycle_1h

# Strategy keeps its cash in USDC
RESERVE_CURRENCY = ReserveCurrency.busd

# Time bucket for our candles

# Which chain we are trading
CHAIN_ID = ChainId.bsc

# Which exchange we are trading on.
EXCHANGE_SLUG = "pancakeswap-v2"

# Which trading pair we are trading

# How much of the cash to put on a single trade

# Strategy thinking specific parameter



# Range of backtesting and synthetic data generation.
# Because we are using synthetic data actual dates do not really matter -
# only the duration

START_AT = datetime.datetime(2022, 1, 1)

END_AT = datetime.datetime(2022, 1,18)

# Start with 10,000 USD

# If the price drops 0.7% we trigger a stop loss


Deciding Trades#

  • The decide_trades function is used to determine which trades we should make. This is the main function that drives our trading decisions.

Using Indicators#

  • In this case, we’re using two exponential moving averages (EMAs) to help us make these decisions. EMAs are a type of data smoothing that gives more weight to recent data. By comparing two EMAs (one that reacts more quickly to price changes and one that reacts more slowly), we can identify potential trading opportunities.

  • To keep our price chart uncluttered, we use “detached” and “overlayed” technical indicators. This means we present some data in separate panels rather than all on the main price chart.

Event Indicators#

  • We also use “event indicators” to highlight certain key events on the chart. Unlike continuous indicators (like EMAs), these are discrete events that only need to be shown at certain points.

  • To visualize these events, we use “markers” (which look like circles) rather than lines.

  • The functions is_crossover and is_crossover_constant help us create these event indicators. They identify points where our two EMAs cross each other, which can signal a trading opportunity.

  • However, these event indicators might not always align perfectly with the crossover points when you zoom in on the chart. This is due to the fact that our price data is “discrete” (it only updates at specific intervals, like every minute or hour) rather than “continuous” (updating constantly in real time).

  • In simple terms, we’re using some mathematical tools and visual aids to help us decide when to trade. These tools look at recent price changes and highlight key events that might suggest a good opportunity to buy or sell.

from typing import List, Dict

from pandas_ta.overlap import ema

from tradeexecutor.state.visualisation import PlotKind, PlotShape
from tradeexecutor.state.crossover import is_crossover, is_crossover_constant
from tradeexecutor.state.trade import TradeExecution
from tradeexecutor.strategy.pricing_model import PricingModel
from tradeexecutor.strategy.pandas_trader.position_manager import PositionManager
from tradeexecutor.state.state import State
from tradingstrategy.universe import Universe
from tradeexecutor.strategy.pandas_trader.position_manager import PositionManager

import random
def decide_trades(
        timestamp: pd.Timestamp,
        universe: Universe,
        state: State,
        pricing_model: PricingModel,
        cycle_debug_data: Dict) -> List[TradeExecution]:
    """The brain function to decide the trades on each trading strategy cycle.

    - Reads incoming execution state (positions, past trades)

    - Reads the current universe (candles)

    - Decides what trades to do next, if any, at current timestamp.

    - Outputs strategy thinking for visualisation and debug messages

    :param timestamp:
        The Pandas timestamp object for this cycle. Matches
        TRADING_STRATEGY_CYCLE division.
        Always truncated to the zero seconds and minutes, never a real-time clock.

    :param universe:
        Trading universe that was constructed earlier.

    :param state:
        The current trade execution state.
        Contains current open positions and all previously executed trades, plus output
        for statistics, visualisation and diangnostics of the strategy.

    :param pricing_model:
        Pricing model can tell the buy/sell price of the particular asset at a particular moment.

    :param cycle_debug_data:
        Python dictionary for various debug variables you can read or set, specific to this trade cycle.
        This data is discarded at the end of the trade cycle.

        List of trade instructions in the form of :py:class:`TradeExecution` instances.
        The trades can be generated using `position_manager` but strategy could also hand craft its trades.

    # The pair we are trading
    pair = universe.pairs.get_single()

    # How much cash we have in the hand
    cash = state.portfolio.get_current_cash()

    # Get OHLCV candles for our trading pair as Pandas Dataframe.
    # We could have candles for multiple trading pairs in a different strategy,
    # but this strategy only operates on single pair candle.
    # We also limit our sample size to N latest candles to speed up calculations.
    candles: pd.DataFrame = universe.candles.get_single_pair_data(timestamp, sample_count=BATCH_SIZE)

    # We have data for open, high, close, etc.
    # We only operate using candle close values in this strategy.
    close_prices = candles["close"]

    # Calculate exponential moving averages based on slow and fast sample numbers.
    slow_ema_series = ema(close_prices, length=SLOW_EMA_CANDLE_COUNT)
    fast_ema_series = ema(close_prices, length=FAST_EMA_CANDLE_COUNT)

    if slow_ema_series is None or fast_ema_series is None:
        # Cannot calculate EMA, because
        # not enough samples in backtesting
        return []

    if len(slow_ema_series) < 2 or len(fast_ema_series) < 2:
        # We need at least two data points to determine if EMA crossover (or crossunder)
        # occurred at current timestamp.
        return []

    slow_ema_latest = slow_ema_series.iloc[-1]
    fast_ema_latest = fast_ema_series.iloc[-1]
    price_latest = close_prices.iloc[-1]

    # Compute technical indicators needed for trade decisions.
    slow_ema_crossover = (
        close_prices.iloc[-3] < slow_ema_series.iloc[-2]
        and price_latest > slow_ema_latest
    slow_ema_crossunder = (
        close_prices.iloc[-2] > slow_ema_series.iloc[-2]
        and price_latest < slow_ema_latest
    fast_ema_crossunder = (
        close_prices.iloc[-2] > fast_ema_series.iloc[-2]
        and price_latest < fast_ema_latest

#    entry_price = tradeposition.open_price
#    entry_price = float(get_buy_price['amount'])
    # position_manager = PositionManager(timestamp, universe, state, pricing_model)

    trades = []

    # entry_price = position_manager.get_current_position().get_current_price()

    # entry_price = tradeexecutor.analysis.trade_analyser.SpotTrade

    # List of any trades we decide on this cycle.
    # trades = []

    # Create a position manager helper class that allows us easily to create
    # opening/closing trades for different positions
    position_manager = PositionManager(timestamp, universe, state, pricing_model)

    if not position_manager.is_any_open():
        # No open positions, decide if BUY in this cycle.
        # We buy if we just crossed over the slow EMA or if this is a very first
        # trading cycle and the price is already above the slow EMA.
        if (
            or price_latest < slow_ema_latest and timestamp == START_AT
            buy_amount = cash * POSITION_SIZE
            new_trades = position_manager.open_1x_long(pair, buy_amount, stop_loss_pct=STOP_LOSS_PCT)
        # We have an open position, decide if SELL in this cycle.
        # We do that if we fall below any of the two moving averages.
        if slow_ema_crossover or (fast_ema_crossunder and  fast_ema_latest > slow_ema_latest) :
            new_trades = position_manager.close_all()
            assert len(new_trades) == 1

        #    current_position = position_manager.get_current_position()
        #    current_price = current_position.get_current_price()
            #if price_latest <= current_price * STOP_LOSS:
            #    print(f"Stop loss. Now {close}, opened at {entry_price}")
            #    new_trades = position_manager.close_all()
            #    assert len(new_trades) == 1
            #    trades.extend(new_trades)

    # Visualize strategy
    # See available Plotly colours here
    # https://community.plotly.com/t/plotly-colours-list/11730/3?u=miohtama
    visualisation = state.visualisation

    visualisation.plot_indicator(timestamp, "random 1", PlotKind.technical_indicator_detached, random.randint(7100, 10000), colour="green")

    visualisation.plot_indicator(timestamp, "Fast EMA", PlotKind.technical_indicator_detached, fast_ema_latest, colour="red")

    visualisation.plot_indicator(timestamp, "Slow EMA", PlotKind.technical_indicator_overlay_on_detached, slow_ema_latest, colour="green", detached_overlay_name="Fast EMA")

    # another way to plot horizontal lines
    # allows you to plot as many as you want
    visualisation.plot_indicator(timestamp, "h2", PlotKind.technical_indicator_overlay_on_detached, 3300, colour="orange", detached_overlay_name="Fast EMA")

    # Event indicators
    if is_crossover(fast_ema_series, slow_ema_series):
        visualisation.plot_indicator(timestamp, "Crossover 1", PlotKind.technical_indicator_overlay_on_detached, fast_ema_latest, colour="blue", detached_overlay_name="Fast EMA", plot_shape=PlotShape.markers)

    if is_crossover_constant(fast_ema_series, 3300):
        visualisation.plot_indicator(timestamp, "Crossover 2", PlotKind.technical_indicator_overlay_on_detached, fast_ema_latest, colour="black", detached_overlay_name="Fast EMA", plot_shape=PlotShape.markers)

    return trades

Defining the trading universe#

We create a trading universe with a single blockchain, exchange and trading pair. For the sake of easier understanding the code, we name this “Uniswap v2” like exchange with a single ETH-USDC trading pair.

The trading pair contains generated noise-like OHLCV trading data.

from typing import Optional
from tradeexecutor.strategy.trading_strategy_universe import load_pair_data_for_single_exchange, TradingStrategyUniverse
from tradeexecutor.strategy.execution_context import ExecutionContext
from tradeexecutor.strategy.universe_model import UniverseOptions
from tradingstrategy.client import Client
import datetime

def create_single_pair_trading_universe(
        ts: datetime.datetime,
        client: Client,
        execution_context: ExecutionContext,
        universe_options: UniverseOptions,
) -> TradingStrategyUniverse:
    dataset = load_pair_data_for_single_exchange(

    # Filter down to the single pair we are interested in
    universe = TradingStrategyUniverse.create_single_pair_universe(

    return universe

Set up the market data client#

The Trading Strategy market data client is the Python library responsible for managing the data feeds needed to run the backtest.None

We set up the market data client with an API key.

If you do not have an API key yet, you can register one.

from tradingstrategy.client import Client

client = Client.create_jupyter_client()
Started Trading Strategy in Jupyter notebook environment, configuration is stored in /home/alex/.tradingstrategy

Load data#

from tradeexecutor.strategy.execution_context import ExecutionMode
from tradeexecutor.strategy.universe_model import UniverseOptions

universe = create_single_pair_trading_universe(
print(f"We loaded {universe.universe.candles.get_candle_count():,} candles.")
We loaded 17,582 candles.

Run backtest#

Run backtest using giving trading universe and strategy function.

  • Running the backtest outputs state object that contains all the information on the backtesting position and trades.

  • The trade execution engine will download the necessary datasets to run the backtest. The datasets may be large, several gigabytes.

import logging

from tradeexecutor.backtest.backtest_runner import run_backtest_inline

state, universe, debug_dump = run_backtest_inline(
    name="ETH/USDC fast and slow EMA example",
    # create_trading_universe=create_single_pair_trading_universe,

trade_count = len(list(state.portfolio.get_all_trades()))
print(f"Backtesting completed, backtested strategy made {trade_count} trades")
Backtesting completed, backtested strategy made 78 trades

Examine backtest results#

Examine state that contains all actions the trade executor took.

We plot out a chart that shows - The price action - When the strategy made buys or sells

print(f"Positions taken: {len(list(state.portfolio.get_all_positions()))}")
print(f"Trades made: {len(list(state.portfolio.get_all_trades()))}")
Positions taken: 39
Trades made: 78


When zooming in, the event indicators will not be exactly where they should, unless the event falls exactly on a candle close. This is unfortunately something that cannot be fixed for the time being.


from tradeexecutor.visual.single_pair import visualise_single_pair, visualise_single_pair_positions_with_duration_and_slippage from tradingstrategy.charting.candle_chart import VolumeBarMode figure = visualise_single_pair( state, universe.universe.candles, start_at=START_AT, end_at=END_AT, volume_bar_mode=VolumeBarMode.separate, volume_axis_name="Volume (USD)", height = 1000, relative_sizing=[1, 0.2, 0.2, 1] ) figure.show()