Tags: live-strategy

Examining live trading strategy using notebook#

In this notebook, we show how to examine live trading strategy instance using Python and Jupyter notebook.

  • Downloading the trades from the live instance

  • Reading data to State object

  • Using the same analytics functions we use in backtests against the live data

Getting started#

First, let’s create Trading Strategy oracle market data client. If you do not have an API key yet, you will be asked to create one.

[1]:
from tradingstrategy.client import Client

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

Download strategy trades#

  • Each strategy Docker instances offers a webhook that allows access the data of this strategy, include State object that is the flat file state of the all the strategy current and past decision making

  • Note that State class and any children classes in the state tree may have changes between versions and decoding might need you to use a specific version of trade-executor

[2]:

from tradeexecutor.monkeypatch.dataclasses_json import patch_dataclasses_json
from tradeexecutor.state.state import State
import requests

# Currently needed because unpatched dataclasses_json package issues
patch_dataclasses_json()

# Public internet endpoint as exposed by the trade executor Docker
webbhook_url = "https://pancake-eth-usd-sma.tradingstrategy.ai"

state_api = f"{webbhook_url}/state"

resp = requests.get(state_api)

state_blob = resp.content

print(f"Downloaded {len(state_blob):,} bytes state data")

state = State.from_json(state_blob)

print(f"trade-executor was launched at: {state.created_at}, we have {len(list(state.portfolio.get_all_trades()))} trades")
Downloaded 6,467 bytes state data
---------------------------------------------------------------------------
JSONDecodeError                           Traceback (most recent call last)
Cell In[2], line 19
     15 state_blob = resp.content
     17 print(f"Downloaded {len(state_blob):,} bytes state data")
---> 19 state = State.from_json(state_blob)
     21 print(f"trade-executor was launched at: {state.created_at}, we have {len(list(state.portfolio.get_all_trades()))} trades")

File ~/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_json/api.py:58, in DataClassJsonMixin.from_json(cls, s, parse_float, parse_int, parse_constant, infer_missing, **kw)
     49 @classmethod
     50 def from_json(cls: Type[A],
     51               s: JsonData,
   (...)
     56               infer_missing=False,
     57               **kw) -> A:
---> 58     kvs = json.loads(s,
     59                      parse_float=parse_float,
     60                      parse_int=parse_int,
     61                      parse_constant=parse_constant,
     62                      **kw)
     63     return cls.from_dict(kvs, infer_missing=infer_missing)

File ~/.pyenv/versions/3.10.8/lib/python3.10/json/__init__.py:346, in loads(s, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    341     s = s.decode(detect_encoding(s), 'surrogatepass')
    343 if (cls is None and object_hook is None and
    344         parse_int is None and parse_float is None and
    345         parse_constant is None and object_pairs_hook is None and not kw):
--> 346     return _default_decoder.decode(s)
    347 if cls is None:
    348     cls = JSONDecoder

File ~/.pyenv/versions/3.10.8/lib/python3.10/json/decoder.py:337, in JSONDecoder.decode(self, s, _w)
    332 def decode(self, s, _w=WHITESPACE.match):
    333     """Return the Python representation of ``s`` (a ``str`` instance
    334     containing a JSON document).
    335
    336     """
--> 337     obj, end = self.raw_decode(s, idx=_w(s, 0).end())
    338     end = _w(s, end).end()
    339     if end != len(s):

File ~/.pyenv/versions/3.10.8/lib/python3.10/json/decoder.py:355, in JSONDecoder.raw_decode(self, s, idx)
    353     obj, end = self.scan_once(s, idx)
    354 except StopIteration as err:
--> 355     raise JSONDecodeError("Expecting value", s, err.value) from None
    356 return obj, end

JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Analyse the live trades performance#

We can use the same functions as in our backtesting notebooks to produce summaries, graphs.

[ ]:
import pandas as pd
from IPython.core.display_functions import display
from tradeexecutor.analysis.trade_analyser import build_trade_analysis

analysis = build_trade_analysis(state.portfolio)

summary = analysis.calculate_summary_statistics()

with pd.option_context("display.max_row", None):
    summary.display()

Determinte strategy trading pairs#

  • Figure out what trading pairs the strategy has been trading from its state

[ ]:
from tradeexecutor.state.position import TradingPosition

pairs = set()

position: TradingPosition
for trade in state.portfolio.get_all_trades():
    pairs.add(trade.pair)


first_trade, last_trade = state.portfolio.get_first_and_last_executed_trade()

print(f"Strategy was trading: {pairs}")

Download market data#

  • We download the trading pair OHCLV candles so that we can overlay the strategy trades on the

  • We use 5 minutes accuracy to be able visualise trade events and slippage more accurately on the price chart

[ ]:
from tradeexecutor.state.identifier import TradingPairIdentifier
from tradingstrategy.timebucket import TimeBucket
import datetime

assert len(pairs) == 1, "In this notebook, we support analysing only single pair strategies"
pair: TradingPairIdentifier
(pair,) = pairs  # Operate on a single pair now on

# Add some data margin around our
# trade timeline visualisation
feed_start_at = first_trade.started_at - datetime.timedelta(days=2)
feed_end_at = last_trade.executed_at + datetime.timedelta(days=2)

candles: pd.DataFrame = client.fetch_candles_by_pair_ids(
    {pair.internal_id},
    TimeBucket.m15,
    progress_bar_description=f"Download data for {pair.base.token_symbol} - {pair.quote.token_symbol}",
    start_time=feed_start_at,
    end_time=feed_end_at,
)

print(f"Loaded {len(candles)} candles, {feed_start_at} - {feed_end_at}")

Visualise positions#

  • We use more accurate 15 minutes candle data do visualite the price action, instead of the strategy decision making 1h cycle. This allows us to examine our trade execution against more precie price movement.

  • We use a specific chart to analyse live trading positions visually.

  • Positions are visualised as area: green is closed for profit, red is closed for loss. Each position has a tooltip hint at the top to show its data.

  • This chart type also shows slippage and duration, between the trade start and trade end, to visualise how much much losses were taken on slippage.

  • Individual trades, entries and exists are visualised as arrows.

  • If arrows have any meaningful length in Y (price) direction it means expected price and execution prices differ. There was slippage and we could not execute the trade with the price we wanted.

  • If arrows have any meaningful length in X (time) direction it means that executing the trade too longer than expected - block and transaction confirmation was slow, or there was some other issue in the trade execution.

  • If the arrows are point like it means the trade was executed as expected.

[ ]:
from tradeexecutor.visual.single_pair import visualise_single_pair_positions_with_duration_and_slippage

fig = visualise_single_pair_positions_with_duration_and_slippage(
    state,
    candles,
    start_at=feed_start_at,
    end_at=feed_end_at,
)

display(fig)
[ ]:
print("All done")