Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/runtests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:
- issues/*
pull_request:
branches:
- master
- master
- develop

jobs:
Expand Down
40 changes: 40 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
default_stages: [pre-commit]
fail_fast: false

repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args:
- --exclude-files
- ^(poetry\.lock|tests/.*)$

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
- id: check-added-large-files
args: ["--maxkb=1024"]
- id: check-merge-conflict
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-json
- id: check-yaml
- id: check-toml
- id: debug-statements
- id: check-ast

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.5
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format

- repo: https://github.com/PyCQA/bandit
rev: 1.8.6
hooks:
- id: bandit
args: [-c, pyproject.toml, -q, -r, basana, samples]
pass_filenames: false
2 changes: 1 addition & 1 deletion DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Instead of running those commands manually, a couple of Invoke tasks are provide
$ inv create-virtualenv
```

1. Execute static checks and testcases
1. Execute static checks and testcases

```
$ inv test
Expand Down
1 change: 0 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
$ pip install basana[charts]
```

If you want to use the Hyperliquid connector as well, install the additional
exchange dependencies:

```
$ pip install hyperliquid-python-sdk eth-account
```

The examples use [TALIpp](https://github.com/nardew/talipp) for the technical indicators, pandas, statsmodels and also [Textual](https://textual.textualize.io/) if you want to run the Binance order book mirror.

```
Expand Down
1 change: 0 additions & 1 deletion basana/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,3 @@
from .core.token_bucket import (
TokenBucketLimiter,
)

51 changes: 31 additions & 20 deletions basana/backtesting/account_balances.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,26 @@
class UpdateRule(metaclass=abc.ABCMeta):
@abc.abstractmethod
def check(
self, updated_balances: ValueMap, updated_holds: ValueMap, updated_borrowed: ValueMap,
delta_balances: ValueMap, delta_holds: ValueMap, delta_borrowed: ValueMap
self,
updated_balances: ValueMap,
updated_holds: ValueMap,
updated_borrowed: ValueMap,
delta_balances: ValueMap,
delta_holds: ValueMap,
delta_borrowed: ValueMap,
):
raise NotImplementedError()


class NonZero(UpdateRule):
def check(
self, updated_balances: ValueMap, updated_holds: ValueMap, updated_borrowed: ValueMap,
delta_balances: ValueMap, delta_holds: ValueMap, delta_borrowed: ValueMap
self,
updated_balances: ValueMap,
updated_holds: ValueMap,
updated_borrowed: ValueMap,
delta_balances: ValueMap,
delta_holds: ValueMap,
delta_borrowed: ValueMap,
):
# balance >= 0
for symbol, value in updated_balances.items():
Expand All @@ -54,8 +64,13 @@ def check(
class ValidHold(UpdateRule):
# * hold <= balance
def check(
self, updated_balances: ValueMap, updated_holds: ValueMap, updated_borrowed: ValueMap,
delta_balances: ValueMap, delta_holds: ValueMap, delta_borrowed: ValueMap
self,
updated_balances: ValueMap,
updated_holds: ValueMap,
updated_borrowed: ValueMap,
delta_balances: ValueMap,
delta_holds: ValueMap,
delta_borrowed: ValueMap,
):
symbols = set(itertools.chain(updated_holds.keys(), updated_balances.keys()))
for symbol in symbols:
Expand All @@ -67,33 +82,29 @@ def check(

class AccountBalances:
def __init__(self, initial_balances: ValueMapDict):
self.balances = ValueMap({
symbol: balance for symbol, balance in initial_balances.items() if balance >= 0
})
self.balances = ValueMap({symbol: balance for symbol, balance in initial_balances.items() if balance >= 0})
self.holds = ValueMap()
self.borrowed = ValueMap({
symbol: -balance for symbol, balance in initial_balances.items() if balance < 0
})
self._update_rules: List[UpdateRule] = [
NonZero(),
ValidHold()
]
self.borrowed = ValueMap({symbol: -balance for symbol, balance in initial_balances.items() if balance < 0})
self._update_rules: List[UpdateRule] = [NonZero(), ValidHold()]

def push_update_rule(self, update_rule: UpdateRule):
self._update_rules.append(update_rule)

def update(
self, balance_updates: ValueMapDict = {}, hold_updates: ValueMapDict = {},
borrowed_updates: ValueMapDict = {}
self, balance_updates: ValueMapDict = {}, hold_updates: ValueMapDict = {}, borrowed_updates: ValueMapDict = {}
):
updated_balances = self.balances + balance_updates
updated_holds = self.holds + hold_updates
updated_borrowed = self.borrowed + borrowed_updates

for rule in self._update_rules:
rule.check(
updated_balances, updated_holds, updated_borrowed,
ValueMap(balance_updates), ValueMap(hold_updates), ValueMap(borrowed_updates)
updated_balances,
updated_holds,
updated_borrowed,
ValueMap(balance_updates),
ValueMap(hold_updates),
ValueMap(borrowed_updates),
)

# Update if no error ocurred.
Expand Down
47 changes: 33 additions & 14 deletions basana/backtesting/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class DataPointFromSequence:

:param seq: The sequence that will be used to get the value.
"""

def __init__(self, seq: Sequence[Any]):
self._seq = seq

Expand Down Expand Up @@ -102,10 +103,16 @@ def add_traces(self, figure: go.Figure, row: int):
close_values = list(self._close_values.get_x_y())
figure.add_trace(
go.Candlestick(
x=open_values[0], open=open_values[1], high=high_values[1], low=low_values[1],
close=close_values[1], name=str(self._pair), xperiodalignment="start"
x=open_values[0],
open=open_values[1],
high=high_values[1],
low=low_values[1],
close=close_values[1],
name=str(self._pair),
xperiodalignment="start",
),
row=row, col=1
row=row,
col=1,
)
else:
# Add a trace just with the closing values.
Expand All @@ -117,21 +124,29 @@ def add_traces(self, figure: go.Figure, row: int):
x, y = self._get_order_fills(OrderOperation.BUY).get_x_y()
figure.add_trace(
go.Scatter(
x=x, y=y, name="Buy", mode="markers",
marker=dict(symbol="arrow-up", color="green", size=self.buy_marker_size)
x=x,
y=y,
name="Buy",
mode="markers",
marker=dict(symbol="arrow-up", color="green", size=self.buy_marker_size),
),
row=row, col=1
row=row,
col=1,
)

# Add a trace with sell prices.
if self._include_sells:
x, y = self._get_order_fills(OrderOperation.SELL).get_x_y()
figure.add_trace(
go.Scatter(
x=x, y=y, name="Sell", mode="markers",
marker=dict(symbol="arrow-down", color="red", size=self.sell_marker_size)
x=x,
y=y,
name="Sell",
mode="markers",
marker=dict(symbol="arrow-down", color="red", size=self.sell_marker_size),
),
row=row, col=1
row=row,
col=1,
)

# Add one trace per indicator.
Expand All @@ -154,8 +169,7 @@ def add_indicator(self, name: str, get_data_point: ChartDataPointFn, marker: dic
def _get_order_fills(self, op: OrderOperation) -> TimeSeries:
ret = TimeSeries()
orders = filter(
lambda order: order.pair == self._pair and order.operation == op,
self._exchange._get_all_orders()
lambda order: order.pair == self._pair and order.operation == op, self._exchange._get_all_orders()
)
pair_info = self._exchange._get_pair_info(self._pair)
for order in orders:
Expand Down Expand Up @@ -286,6 +300,7 @@ class LineCharts:

:param exchange: The backtesting exchange.
"""

def __init__(self, exchange: Exchange):
self._exchange = exchange
self._balance_charts: Dict[str, AccountBalanceLineChart] = collections.OrderedDict()
Expand Down Expand Up @@ -360,8 +375,12 @@ def show(self, show_legend: bool = True): # pragma: no cover
fig.show()

def save(
self, path: str, width: Optional[int] = None, height: Optional[int] = None,
scale: Optional[Union[int, float]] = None, show_legend: bool = True
self,
path: str,
width: Optional[int] = None,
height: Optional[int] = None,
scale: Optional[Union[int, float]] = None,
show_legend: bool = True,
):
"""Saves the chart to a file.

Expand Down Expand Up @@ -396,7 +415,7 @@ def _build_figure(self, show_legend: bool = True) -> Optional[go.Figure]:
row = 1
for chart in charts:
chart.add_traces(figure, row)
figure.update_xaxes(rangeslider={'visible':False}, row=row, col=1)
figure.update_xaxes(rangeslider={"visible": False}, row=row, col=1)
row += 1

figure.layout.update(showlegend=show_legend)
Expand Down
2 changes: 2 additions & 0 deletions basana/backtesting/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@

class Error(errors.Error):
"""Base class for backtesting exceptions."""

pass


class NotEnoughBalance(Error):
"""Not enough balance."""

pass


Expand Down
Loading