Skip to content

Commit

Permalink
Docs update. Code refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
pawelkn committed Jan 16, 2024
1 parent 9228f27 commit b412a5b
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 21 deletions.
2 changes: 1 addition & 1 deletion btester/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"""

name = "btester"
__version__ = "0.0.2"
__version__ = "0.1.0"

from .btester import *
66 changes: 57 additions & 9 deletions btester/btester.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class Strategy(ABC):
- trades: List[Trade] - List of completed trades during backtesting.
- open_positions: List[Position] - List of remaining open positions during backtesting.
- cumulative_return: float - Cumulative return of the strategy.
- cash_stock_value: float - Sum of cash and market value of open positions.
- assets_value: float - Market value of open positions.
Methods:
- open(self, price: float, size: Optional[float] = None, symbol: Optional[str] = None) -> bool
Expand All @@ -112,11 +112,59 @@ class Strategy(ABC):

@abstractmethod
def init(self):
""" Abstract method for initializing resources for the strategy """
"""
Abstract method for initializing resources and parameters for the strategy.
This method is called once at the beginning of the backtest to perform any necessary setup or configuration
for the trading strategy. It allows the strategy to initialize variables, set parameters, or load external data
needed for the strategy's functionality.
Parameters:
- *args: Additional positional arguments that can be passed during initialization.
- **kwargs: Additional keyword arguments that can be passed during initialization.
Example:
```python
def init(self, buy_period: int, sell_period: int):
self.buy_signal = {}
self.sell_signal = {}
for symbol in self.symbols:
self.buy_signal[symbol] = UpBreakout(self.data[(symbol,'Close')], buy_period)
self.sell_signal[symbol] = DownBreakout(self.data[(symbol,'Close')], sell_period)
```
Note:
It is recommended to define the expected parameters and their default values within the `init` method
to allow flexibility and customization when initializing the strategy.
"""

@abstractmethod
def next(self, i: int, record: Dict[Hashable, Any]):
""" Abstract method defining the core functionality of the strategy """
"""
Abstract method defining the core functionality of the strategy for each time step.
This method is called iteratively for each time step during the backtest, allowing the strategy to make
decisions based on the current market data represented by the 'record'. It defines the core logic of the
trading strategy, such as generating signals, managing positions, and making trading decisions.
Parameters:
- i (int): Index of the current time step.
- record (Dict[Hashable, Any]): Dictionary representing the market data at the current time step.
The keys can include symbols, and the values can include relevant market data (e.g., OHLC prices).
Example:
```python
def next(self, i, record):
for symbol in self.symbols:
if self.buy_signal[symbol][i-1]:
self.open(symbol=symbol, price=record[(symbol,'Open')], size=self.positionSize(record[(symbol,'Open')]))
for position in self.open_positions[:]:
if self.sell_signal[position.symbol][i-1]:
self.close(position=position, price=record[(position.symbol,'Open')])
```
"""

def __init__(self):
self.data = pd.DataFrame()
Expand All @@ -134,7 +182,7 @@ def __init__(self):
self.open_positions: List[Position] = []

self.cumulative_return = self.cash
self.cash_stock_value = .0
self.assets_value = .0

def open(self, price: float, size: Optional[float] = None, symbol: Optional[str] = None):
"""
Expand Down Expand Up @@ -167,7 +215,7 @@ def open(self, price: float, size: Optional[float] = None, symbol: Optional[str]
position = Position(symbol=symbol, open_date=self.date, open_price=price, position_size=size)
position.update(last_date=self.date, last_price=price)

self.cash_stock_value += position.current_value
self.assets_value += position.current_value
self.cash -= open_cost

self.open_positions.extend([position])
Expand Down Expand Up @@ -198,7 +246,7 @@ def close(self, price: float, symbol: Optional[str] = None, position: Optional[P
if position.symbol == symbol:
self.close(position=position, price=price)
else:
self.cash_stock_value -= position.current_value
self.assets_value -= position.current_value
position.update(last_date=self.date, last_price=price)

trade_commission = (position.open_price + position.last_price) * position.position_size * self.commission
Expand All @@ -218,7 +266,7 @@ def close(self, price: float, symbol: Optional[str] = None, position: Optional[P

def __eval(self, *args, **kwargs):
self.cumulative_return = self.cash
self.cash_stock_value = .0
self.assets_value = .0

self.init(*args, **kwargs)

Expand All @@ -232,8 +280,8 @@ def __eval(self, *args, **kwargs):
if last_price > 0:
position.update(last_date=self.date, last_price=last_price)

self.cash_stock_value = sum(position.current_value for position in self.open_positions)
self.returns.append(self.cash + self.cash_stock_value)
self.assets_value = sum(position.current_value for position in self.open_positions)
self.returns.append(self.cash + self.assets_value)

return Result(
returns=pd.Series(index=self.index, data=self.returns, dtype=float),
Expand Down
2 changes: 1 addition & 1 deletion examples/multiple-assets-brakeout.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
" self.close(position=position, price=record[(position.symbol,'Open')])\n",
"\n",
" def positionSize(self, price: float):\n",
" return round((self.cash + self.cash_stock_value) / price * self.buy_at_once_size) if price > 0 else 0"
" return round((self.cash + self.assets_value) / price * self.buy_at_once_size) if price > 0 else 0"
]
},
{
Expand Down
6 changes: 3 additions & 3 deletions examples/multiple-assets-ma-crossover.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@
" def next(self, i, record):\n",
" for symbol in self.symbols:\n",
" if self.fast_ma[symbol][i-1] > self.slow_ma[symbol][i-1]:\n",
" self.open(symbol=symbol, price=record[(symbol,'Open')], size=self._positionSize(record[(symbol,'Open')]))\n",
" self.open(symbol=symbol, price=record[(symbol,'Open')], size=self.positionSize(record[(symbol,'Open')]))\n",
"\n",
" for position in self.open_positions[:]:\n",
" if self.fast_ma[position.symbol][i-1] < self.slow_ma[position.symbol][i-1]:\n",
" self.close(position=position, price=record[(position.symbol,'Open')])\n",
"\n",
" def _positionSize(self, price: float):\n",
" return round((self.cash + self.cash_stock_value) / price * self.buy_at_once_size) if price > 0 else 0"
" def positionSize(self, price: float):\n",
" return round((self.cash + self.assets_value) / price * self.buy_at_once_size) if price > 0 else 0"
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "btester"
version = "0.0.2"
version = "0.1.0"
authors = [
{ name="Paweł Knioła", email="pawel.kn@gmail.com" },
]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
"Programming Language :: Python :: 3.12"
],
python_requires='>=3.7',
version="0.0.2",
version="0.1.0",
packages=['btester'],
)
44 changes: 39 additions & 5 deletions tests/test_strategy.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

from btester import Strategy
from math import nan, inf

from btester import Strategy, Position, Trade
from math import nan
import pandas as pd
import unittest

class CustomStrategy(Strategy):
Expand All @@ -13,6 +13,21 @@ def next(self, i, record):

class TestStrategy(unittest.TestCase):

def test_initial_states(self):
strategy = CustomStrategy()
pd.testing.assert_frame_equal(strategy.data, pd.DataFrame())
self.assertIsNone(strategy.date)
self.assertEqual(strategy.cash, 0)
self.assertEqual(strategy.commission, 0)
self.assertEqual(strategy.symbols, [])
self.assertEqual(strategy.records, [])
self.assertEqual(strategy.index, [])
self.assertEqual(strategy.returns, [])
self.assertEqual(strategy.trades, [])
self.assertEqual(strategy.open_positions, [])
self.assertEqual(strategy.cumulative_return, 0)
self.assertEqual(strategy.assets_value, 0)

# The init() method can be overridden by a subclass of Strategy
def test_init_method_overridden(self):
strategy = CustomStrategy()
Expand All @@ -30,19 +45,38 @@ def test_open_returns_false_if_price_or_size_invalid(self):
self.assertFalse(strategy.open(10, nan))
self.assertFalse(strategy.open(0, 10))
self.assertFalse(strategy.open(10, 0))
self.assertEqual(strategy.open_positions, [])

# The open() method returns False if there is not enough cash to cover the cost of the trade
def test_open_returns_false_if_not_enough_cash(self):
strategy = CustomStrategy()
strategy.cash = 100
self.assertFalse(strategy.open(10, 20))
self.assertEqual(strategy.open_positions, [])

# The open() method sets size to cash / price if size is none
def test_open_sets_size_to_cash_divided_by_price_if_size_is_none(self):
strategy = CustomStrategy()
strategy.cash = 100
self.assertTrue(strategy.open(10, None))
self.assertEqual(strategy.open_positions[0].position_size, 10)

self.assertTrue(strategy.open(10)) # open a new position
self.assertEqual(strategy.cash, 0)
self.assertEqual(strategy.trades, [])
self.assertEqual(strategy.open_positions, [
Position(symbol=None, open_date=None, last_date=None, open_price=10, last_price=10,
position_size=10.0, profit_loss=0.0, change_pct=0.0, current_value=100.0)
])
self.assertEqual(strategy.assets_value, 100)

self.assertTrue(strategy.close(20)) # close the position
self.assertEqual(strategy.cash, 200)
self.assertEqual(strategy.trades, [
Trade(symbol=None, open_date=None, close_date=None, open_price=10, close_price=20,
position_size=10.0, profit_loss=100.0, change_pct=100.0, trade_commission=0.0,
cumulative_return=100.0)
])
self.assertEqual(strategy.open_positions, [])
self.assertEqual(strategy.assets_value, 0)

# The close() method returns False if price is NaN or less than or equal to zero
def test_close_returns_false_if_price_or_size_invalid(self):
Expand Down

0 comments on commit b412a5b

Please sign in to comment.