Skip to content

Commit

Permalink
Ergonomy fixes to backtest command (#1132)
Browse files Browse the repository at this point in the history
- Small usability improvements
- Clean backtest report for warnings
- Add `decide_trades()` messages to the backtesting report template
- Add asset weight allocation to the backtesting report  template
  • Loading branch information
miohtama authored Jan 8, 2025
1 parent 72c87da commit 0d0f3f5
Show file tree
Hide file tree
Showing 12 changed files with 1,062 additions and 22 deletions.
2 changes: 1 addition & 1 deletion deps/trading-strategy
2 changes: 1 addition & 1 deletion spec
Submodule spec updated 1 files
+275 −0 open-defi-api.yaml
946 changes: 946 additions & 0 deletions strategies/base-memex.py

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions tests/lagoon/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ def vault_strategy_universe(
@pytest.fixture()
def automated_lagoon_vault(
web3,
deployer_local_account,
deployer_hot_wallet,
asset_manager,
multisig_owners,
uniswap_v2,
Expand All @@ -425,7 +425,7 @@ def automated_lagoon_vault(
"""

chain_id = web3.eth.chain_id
deployer = deployer_local_account
deployer = deployer_hot_wallet

parameters = LagoonDeploymentParameters(
underlying=USDC_NATIVE_TOKEN[chain_id],
Expand Down Expand Up @@ -528,10 +528,16 @@ def uniswap_v3(web3) -> UniswapV3Deployment:


@pytest.fixture()
def deployer_local_account(web3) -> LocalAccount:
def deployer_hot_wallet(web3) -> HotWallet:
"""Account that we use for Lagoon deployment"""
hot_wallet = HotWallet.create_for_testing(web3, eth_amount=1)
return hot_wallet.account
return hot_wallet


@pytest.fixture()
def deployer_local_account(deployer_hot_wallet) -> LocalAccount:
"""Account that we use for Lagoon deployment"""
return deployer_hot_wallet.account


@pytest.fixture()
Expand Down
2 changes: 1 addition & 1 deletion tradeexecutor/analysis/multi_asset_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def compare_strategy_backtest_to_multiple_assets(
index=index
)

prices_table = prices_table.applymap(lambda x: f"{x:.2f}" if isinstance(x, (float, np.float64)) else x)
prices_table = prices_table.map(lambda x: f"{x:.2f}" if isinstance(x, (float, np.float64)) else x)
prices_table = prices_table.fillna("-")

table = pd.concat([table, prices_table])
Expand Down
14 changes: 8 additions & 6 deletions tradeexecutor/analysis/weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@


def calculate_asset_weights(
state: State,
state: State,
) -> pd.Series:
"""Get timeline of asset weights for a backtest.
- Designed for visualisation / human readable output
- Might not handle complex cases correctly
- Uses portfolio positions as the input
:return:
Pandas Series of asset weights
Expand Down Expand Up @@ -69,11 +71,11 @@ def calculate_asset_weights(


def visualise_weights(
weights_series: pd.Series,
normalised=True,
color_palette=colors.qualitative.Light24,
template="plotly_dark",
include_reserves=True,
weights_series: pd.Series,
normalised=True,
color_palette=colors.qualitative.Light24,
template="plotly_dark",
include_reserves=True,
) -> Figure:
"""Draw a chart of weights.
Expand Down
69 changes: 68 additions & 1 deletion tradeexecutor/backtest/backtest_report_template.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,34 @@
"collapsed": false
}
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Asset weights\n",
"\n",
"Show weights of different assets during the backtest period."
]
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"from tradeexecutor.analysis.weights import calculate_asset_weights, visualise_weights\n",
"\n",
"weights_series = calculate_asset_weights(state)\n",
"\n",
"fig = visualise_weights(\n",
" weights_series,\n",
" normalised=False,\n",
" include_reserves=True,\n",
" template=\"plotly_white\"\n",
")\n",
"fig.show()"
]
},
{
"cell_type": "markdown",
"source": [
Expand Down Expand Up @@ -362,6 +390,39 @@
"collapsed": false
}
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Trade notes \n",
"\n",
"Examine the trade notes of the last few decision cycles in the backtest. "
]
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"messages = state.visualisation.get_messages_tail(3)\n",
"\n",
"if len(messages) > 0:\n",
" table = pd.Series(\n",
" data=list(messages.values()),\n",
" index=list(messages.keys()),\n",
" )\n",
" \n",
" df = table.to_frame()\n",
" \n",
" display(df.style.set_properties(**{\n",
" 'text-align': 'left',\n",
" 'white-space': 'pre-wrap',\n",
" }))\n",
"else:\n",
" print(\"No trade notes available: decide_trades() did not call add_message()\")"
]
},
{
"cell_type": "markdown",
"source": [
Expand All @@ -380,6 +441,8 @@
"source": [
"from tradeexecutor.analysis.trade_analyser import expand_timeline\n",
"\n",
"limit = 100\n",
"\n",
"if universe.has_lending_data():\n",
" print(\"TODO: Currently disabled for lending-based strategies. Need to add lending data export.\")\n",
"else:\n",
Expand All @@ -389,7 +452,11 @@
" universe.universe.exchanges,\n",
" universe.universe.pairs,\n",
" timeline)\n",
"\n",
" \n",
" if len(expanded_timeline) > limit:\n",
" print(f\"We have {len(expanded_timeline)} entries, displaying only last {limit}\")\n",
" expanded_timeline = expanded_timeline.iloc[-limit:]\n",
" \n",
" # Do not truncate the row output\n",
" with pd.option_context(\"display.max_row\", None):\n",
" display(apply_styles(expanded_timeline))"
Expand Down
1 change: 1 addition & 0 deletions tradeexecutor/cli/commands/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def loop():
execution_context=standalone_backtest_execution_context,
max_workers=max_workers,
cache_path=cache_path,
verbose=not unit_testing,
)
return result

Expand Down
7 changes: 5 additions & 2 deletions tradeexecutor/cli/result.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Simple command line based backtesting result display."""
import traceback

import pandas as pd
from IPython.core.display_functions import display

from tradeexecutor.analysis.trade_analyser import build_trade_analysis
Expand Down Expand Up @@ -29,7 +30,8 @@ def display_backtesting_results(
analysis = build_trade_analysis(state.portfolio)
try:
summary = analysis.calculate_summary_statistics(state=state)
display(summary.to_dataframe(format_headings=False))
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
display(summary.to_dataframe(format_headings=False))
except Exception as e:
print("Could not calculate summary:", e)
traceback.print_exc()
Expand All @@ -41,7 +43,8 @@ def display_backtesting_results(
strategy_universe,
display=True,
)
display(portfolio_comparison)
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
display(portfolio_comparison)

key_metrics = calculate_key_metrics(
State(),
Expand Down
15 changes: 14 additions & 1 deletion tradeexecutor/strategy/alpha_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from dataclasses_json import dataclass_json

from tradeexecutor.state.size_risk import SizeRisk
from tradeexecutor.strategy.execution_context import ExecutionContext
from tradeexecutor.strategy.size_risk_model import SizeRiskModel
from tradingstrategy.types import PrimaryKey

Expand Down Expand Up @@ -1072,6 +1073,7 @@ def generate_rebalance_trades_and_triggers(
individual_rebalance_min_threshold: USDollarAmount = 0.0,
use_spot_for_long=True,
invidiual_rebalance_min_threshold=None,
execution_context: ExecutionContext = None,
) -> List[TradeExecution]:
"""Generate the trades that will rebalance the portfolio.
Expand Down Expand Up @@ -1104,6 +1106,9 @@ def generate_rebalance_trades_and_triggers(
If set False, use leveraged long.
:param execution_context:
Needed to tune down print/log/state file clutter when backtesting thousands of trades.
:return:
List of trades we need to execute to reach the target portfolio.
The sells are sorted always before buys.
Expand Down Expand Up @@ -1275,6 +1280,14 @@ def generate_rebalance_trades_and_triggers(
# Increase or decrease the position for the target pair
# Open new position if needed.
logger.info("Adjusting spot position")

# For long backtests, state file is filled with these notes,
# so we only enable for live trading
notes = ""
if execution_context:
if execution_context.mode.is_live_trading():
notes = f"Resizing position, trade based on signal: {signal} as {self.timestamp}"

position_rebalance_trades += position_manager.adjust_position(
synthetic,
dollar_diff,
Expand All @@ -1284,7 +1297,7 @@ def generate_rebalance_trades_and_triggers(
take_profit=signal.take_profit,
trailing_stop_loss=signal.trailing_stop_loss,
override_stop_loss=self.override_stop_loss,
notes="Rebalance for signal {signal}"
notes=notes,
)
else:
raise NotImplementedError(f"Leveraged long missing w/leverage {signal.leverage}, {signal.get_flip_label()}: {signal}")
Expand Down
4 changes: 2 additions & 2 deletions tradeexecutor/strategy/pandas_trader/strategy_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import logging
from dataclasses import dataclass
from functools import lru_cache
from typing import Callable

import cachetools
Expand All @@ -24,7 +23,6 @@
from tradingstrategy.candle import CandleSampleUnavailable
from tradingstrategy.liquidity import LiquidityDataUnavailable
from tradingstrategy.pair import HumanReadableTradingPairDescription
from tradingstrategy.timebucket import TimeBucket
from tradingstrategy.utils.time import get_prior_timestamp, ZERO_TIMEDELTA

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -468,6 +466,8 @@ def create_indicators(parameters: StrategyParameters, indicators: IndicatorSet,
raise IndicatorDataNotFoundWithinDataTolerance(
f"Asked indicator {name}. Data delay tolerance is {data_delay_tolerance}, but the delay was longer {distance}.\n"
f"Our timestamp {self.timestamp}, fixed timestamp {shifted_ts}, data available at {before_match_timestamp}.\n"
f"First indicator entry {series.index[0]}, last indicator entry {series.index[-1]}.\n"
f"If your indicator data is gappy, make sure you use reindex() or ffill() to fill the gaps.\n"
)

value = before_match
Expand Down
8 changes: 5 additions & 3 deletions tradeexecutor/visual/equity_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,11 @@ def visualise_returns_distribution(
"""

qs = import_quantstats_wrapped()
fig = qs.plots.distribution(
returns,
show=False)
with warnings.catch_warnings(): # DeprecationWarning: Importing display from IPython.core.display is deprecated since IPython 7.14, please import from IPython display
warnings.simplefilter(action='ignore', category=FutureWarning)
fig = qs.plots.distribution(
returns,
show=False)
return fig


Expand Down

0 comments on commit 0d0f3f5

Please sign in to comment.