Skip to content

Commit

Permalink
Merge pull request #3 from bsdz/plotly_strat_runner
Browse files Browse the repository at this point in the history
Plotly strat runner
  • Loading branch information
bsdz authored Jul 4, 2023
2 parents bdf7831 + 004c560 commit e8ba225
Show file tree
Hide file tree
Showing 15 changed files with 22,693 additions and 857 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Run isort
run: poetry run isort . --check-only --profile black
- name: Run docformatter
run: poetry run docformatter . --recursive --check
run: poetry run docformatter . --recursive --check --diff --black --exclude _unittest_numpy_extensions.py
# - name: Run flake8
# run: poetry run flake8 .
# - name: Run bandit
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,5 @@ Before commit run following format commands in project folder:
```bash
poetry run black .
poetry run isort . --profile black
poetry run docformatter . --recursive --in-place
poetry run docformatter . --recursive --in-place --black --exclude _unittest_numpy_extensions.py
```
21,712 changes: 21,697 additions & 15 deletions notebooks/Readme_Example.ipynb

Large diffs are not rendered by default.

1,598 changes: 790 additions & 808 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "yabte"
version = "0.3.5"
version = "0.3.6"
description = "Yet another backtesting engine"
authors = ["Blair Azzopardi <blairuk@gmail.com>"]
license = "MIT"
Expand Down Expand Up @@ -39,6 +39,7 @@ optional = true

[tool.poetry.group.notebooks.dependencies]
matplotlib = "^3.6.2"
plotly = "^5.10.0"
ipykernel = "^6.20.2"
pyfeng = "^0.2.5"
nbconvert = "^7.2.9"
Expand Down
Binary file modified readme_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions yabte/backtest/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ def check_and_fix_data(self, data: pd.DataFrame) -> pd.DataFrame:
@mypyc_attr(allow_interpreted_subclasses=True)
@dataclass(kw_only=True)
class Asset(AssetBase):
"""Assets whose price history is represented by High, Low, Open, Close and
Volume fields."""
"""Assets whose price history is represented by High, Low, Open, Close and Volume
fields."""

@property
def fields_available_at_open(self) -> Sequence[str]:
Expand Down
11 changes: 5 additions & 6 deletions yabte/backtest/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ class OrderBase:
key: Optional[str] = None
"""Unique key for this order.
If a key is set then only the newest order with this key is kept.
Older orders with the same key will be removed.
If a key is set then only the newest order with this key is kept. Older orders with
the same key will be removed.
"""

def __post_init__(self):
Expand All @@ -94,8 +94,7 @@ def _book_trades(self, trades):
def post_complete(self, trades: List[Trade]):
"""Called after and with trades that have been successfully booked.
It can append new orders to suborders for execution in the
following timestep.
It can append new orders to suborders for execution in the following timestep.
"""
pass

Expand Down Expand Up @@ -149,8 +148,8 @@ def _calc_quantity_price(self, day_data, asset_map) -> Tuple[Decimal, Decimal]:
def pre_execute_check(
self, ts: pd.Timestamp, trade_price: Decimal
) -> Optional[OrderStatus]:
"""Called with the current timestep and calculated trade price before
the trade is executed.
"""Called with the current timestep and calculated trade price before the trade
is executed.
If it returns `None`, the trade is executed as normal. It can
return `OrderStatus.CANCELLED` to indicate the trade should be
Expand Down
14 changes: 6 additions & 8 deletions yabte/backtest/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ def sort_by_priority(self):
def remove_duplicate_keys(self) -> List[OrderBase]:
"""Remove older orders with same key.
Returns a list of orders than were removed with status set to
REPLACED.
Returns a list of orders than were removed with status set to REPLACED.
"""
removed = []
cntr = Counter(o.key for o in self.deque if o.key is not None)
Expand Down Expand Up @@ -210,11 +209,11 @@ class StrategyRunner:
"""

data: pd.DataFrame = field()
"""Dataframe of price data including columns High, Low, Open, Close, Volume
for each asset.
"""Dataframe of price data including columns High, Low, Open, Close, Volume for each
asset.
Both asset name and field make a multiindex column. The index should
consist of order pandas timestamps.
Both asset name and field make a multiindex column. The index should consist of
order pandas timestamps.
"""

assets: List[Asset]
Expand All @@ -232,8 +231,7 @@ class StrategyRunner:
books: List[Book] = field(default_factory=list)
"""Books available to strategies.
If not supplied will be populated with single book named 'Main'
denominated in USD.
If not supplied will be populated with single book named 'Main' denominated in USD.
"""

@property
Expand Down
10 changes: 5 additions & 5 deletions yabte/tests/_unittest_numpy_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@


class NumpyTestCase(unittest.TestCase):
"""Specialized TestCase which includes numpy test assertion functions and
maps them from assert_func_name to self.numpyAssertFuncName.
"""Specialized TestCase which includes numpy test assertion functions and maps them
from assert_func_name to self.numpyAssertFuncName.
class NumpyTest(NumpyTestCase):
def test_allclose_example(self):
a1 = np.array([1.,2.,3.])
self.numpyAssertAllclose(a1, np.array([1.,2.,3.1]))
def test_allclose_example(self):
a1 = np.array([1.,2.,3.])
self.numpyAssertAllclose(a1, np.array([1.,2.,3.1]))
"""


Expand Down
7 changes: 3 additions & 4 deletions yabte/utilities/plot/matplotlib/strategy_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@
def plot_strategy_runner(sr: StrategyRunner, settings: dict[str, Any] | None = None):
"""Display the results of a strategy run using matplotlib.
Plots a grid of charts with each column representing a book and rows
representing each asset's price series along with long/short
positioning and volumne series. A bottom row shows the value of each
book as a price series.
Plots a grid of charts with each column representing a book and rows representing
each asset's price series along with long/short positioning and volumne series. A
bottom row shows the value of each book as a price series.
"""
default_settings = {
"candle_body_width": 0.8,
Expand Down
Empty file.
175 changes: 175 additions & 0 deletions yabte/utilities/plot/plotly/strategy_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from typing import Any

import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots

from ....backtest import StrategyRunner


def plot_strategy_runner(sr: StrategyRunner, settings: dict[str, Any] | None = None):
"""Display the results of a strategy run using plotly.
Plots a grid of charts with each column representing a book and rows representing
each asset's price series along with long/short positioning and volumne series. A
bottom row shows the value of each book as a price series.
"""
default_settings: dict[str, Any] = {}

if isinstance(settings, dict):
default_settings = default_settings | settings
s = pd.Series(default_settings, dtype=object)

traded_assets = [
a for a in sr.assets if a.name in sr.transaction_history.asset_name.unique()
]

dpi = 100
col_width = 8 * dpi
row_unit_height = 3 * dpi
ncols = len(sr.books)
nrows = 1 + len(traded_assets)

subplot_titles = [a.name for a in (traded_assets)] + ["Book Value"]
row_heights = [10 for a in (traded_assets)] + [2]
specs = [[{"secondary_y": True} for c in range(ncols)] for r in range(nrows)]

fig = make_subplots(
rows=nrows,
cols=ncols,
shared_xaxes=True,
subplot_titles=subplot_titles,
row_heights=row_heights,
start_cell="top-left",
specs=specs,
vertical_spacing=0.05,
)

for col, book in enumerate(sr.books, start=1):
for row, asset in enumerate(traded_assets, start=1):
prices = sr.data[asset.data_label]

fig.add_trace(
go.Candlestick(
x=prices.index,
open=prices.Open,
high=prices.High,
low=prices.Low,
close=prices.Close,
),
row=row,
col=col,
secondary_y=True,
)

fig.add_trace(
go.Bar(
x=prices.index,
y=prices.Volume,
marker_color="lightgrey",
),
row=row,
col=col,
secondary_y=False,
)

fig.update_xaxes(rangeslider_visible=False, row=row, col=col)
fig.update_yaxes(
title=asset.denom, secondary_y=True, showgrid=True, row=row, col=col
)
fig.update_yaxes(
title="Volume", secondary_y=False, showgrid=False, row=row, col=col
)

# add some range selector buttons
fig.update_xaxes(
rangeselector=dict(
buttons=[
dict(count=1, label="1m", step="month", stepmode="backward"),
dict(count=6, label="6m", step="month", stepmode="backward"),
dict(count=1, label="YTD", step="year", stepmode="todate"),
dict(count=1, label="1y", step="year", stepmode="backward"),
dict(step="all"),
]
),
row=1,
col=col,
)

trans_hist = sr.transaction_history.query(
"asset_name==@asset.name and book==@book.name"
)
pos_hist = (
trans_hist.groupby("ts")
.agg(
quantity=("quantity", np.sum),
labels=(
"order_label",
lambda L: " ".join(l for l in L if l is not None),
),
)
.reindex(prices.index)
)

shorts = prices[pos_hist.eval("quantity < 0")][["Low"]].join(
pos_hist.labels
)
fig.add_trace(
go.Scatter(
x=shorts.index,
y=shorts.Low,
customdata=shorts.labels,
mode="markers",
marker_symbol="arrow-down",
marker_color="red",
marker_size=10,
hovertemplate="%{x}<br>%{y}<br>%{customdata}<extra></extra>",
),
row=row,
col=col,
secondary_y=True,
)

longs = prices[pos_hist.eval("quantity > 0")][["High"]].join(
pos_hist.labels
)
fig.add_trace(
go.Scatter(
x=longs.index,
y=longs.High,
customdata=shorts.labels,
mode="markers",
marker_symbol="arrow-up",
marker_color="green",
marker_size=10,
hovertemplate="%{x}<br>%{y}<br>%{customdata}<extra></extra>",
),
row=row,
col=col,
secondary_y=True,
)

row = nrows
bh = sr.book_history.loc[:, book.name]
fig.add_trace(
go.Scatter(
x=bh.index,
y=bh.total,
),
row=row,
col=col,
)

fig.update_xaxes(rangeslider_visible=True, row=row, col=col)
fig.update_yaxes(title=book.denom, showgrid=True, row=row, col=col)
fig.update_xaxes(title="Date", row=row, col=col)

fig.update_layout(
height=nrows * row_unit_height,
width=ncols * col_width,
showlegend=False,
title_text="Strategy Runner Report",
)

return fig
4 changes: 2 additions & 2 deletions yabte/utilities/portopt/hierarchical_risk_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ def _getRecBipart(cov, sortIx):


def hrp(corr: pd.DataFrame, sigma: np.ndarray) -> np.ndarray:
"""Calculate weights using hierarchical risk parity and scipy's
linkage/to_tree functions."""
"""Calculate weights using hierarchical risk parity and scipy's linkage/to_tree
functions."""
cov = np.diag(sigma) @ corr @ np.diag(sigma)
cov.index, cov.columns = corr.index, corr.columns
rho = corr.values
Expand Down
8 changes: 4 additions & 4 deletions yabte/utilities/portopt/minimum_variance.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def minimum_variance(Sigma: np.ndarray, mu: np.ndarray, r: float) -> np.ndarray:


def minimum_variance_numeric(Sigma: np.ndarray, mu: np.ndarray, r: float) -> np.ndarray:
"""Calculate weights using Lagrangian multipliers and numeric solution
(using scipy's root function)."""
"""Calculate weights using Lagrangian multipliers and numeric solution (using
scipy's root function)."""
m = len(mu)
ones = np.ones(m)

Expand All @@ -52,8 +52,8 @@ def minimum_variance_numeric(Sigma: np.ndarray, mu: np.ndarray, r: float) -> np.
def minimum_variance_numeric_slsqp(
Sigma: np.ndarray, mu: np.ndarray, r: float
) -> np.ndarray:
"""Calculate weights using Lagrangian multipliers and numeric solution
(using scipy's minimize function)."""
"""Calculate weights using Lagrangian multipliers and numeric solution (using
scipy's minimize function)."""
from scipy.optimize import minimize

m = len(mu)
Expand Down

0 comments on commit e8ba225

Please sign in to comment.