diff --git a/.vscode/settings.json b/.vscode/settings.json
index def288c..80005db 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,6 +4,7 @@
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
+ "debugpy.debugJustMyCode": false,
"python.terminal.activateEnvironment": true,
"python.terminal.activateEnvInCurrentTerminal": true,
"python.analysis.exclude": [
diff --git a/lib/PyTrade b/lib/PyTrade
index ed1b101..ef0faea 160000
--- a/lib/PyTrade
+++ b/lib/PyTrade
@@ -1 +1 @@
-Subproject commit ed1b1018c69f0e05aec33e046e724f8804d37d74
+Subproject commit ef0faeac292d271e97b19c159ac3acc0b166522b
diff --git a/poetry.lock b/poetry.lock
index bf01e6f..3f519db 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -571,6 +571,32 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
+[[package]]
+name = "narwhals"
+version = "1.24.1"
+description = "Extremely lightweight compatibility layer between dataframe libraries"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "narwhals-1.24.1-py3-none-any.whl", hash = "sha256:d8983fe14851c95d60576ddca37c094bd4ed24ab9ea98396844fb20ad9aaf184"},
+ {file = "narwhals-1.24.1.tar.gz", hash = "sha256:b09b8253d945f23cdb683a84685abf3afb9f96114d89e9f35dc876e143f65007"},
+]
+
+[package.extras]
+core = ["duckdb", "pandas", "polars", "pyarrow", "pyarrow-stubs"]
+cudf = ["cudf (>=24.10.0)"]
+dask = ["dask[dataframe] (>=2024.8)"]
+dev = ["covdefaults", "hypothesis", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-randomly", "typing-extensions"]
+docs = ["black", "duckdb", "jinja2", "markdown-exec[ansi]", "mkdocs", "mkdocs-autorefs", "mkdocs-material", "mkdocstrings[python]", "pandas", "polars (>=1.0.0)", "pyarrow"]
+duckdb = ["duckdb (>=1.0)"]
+extra = ["scikit-learn"]
+ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"]
+modin = ["modin"]
+pandas = ["pandas (>=0.25.3)"]
+polars = ["polars (>=0.20.3)"]
+pyarrow = ["pyarrow (>=11.0.0)"]
+pyspark = ["pyspark (>=3.5.0)"]
+
[[package]]
name = "numpy"
version = "1.26.4"
@@ -854,6 +880,24 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.11.2)"]
+[[package]]
+name = "plotly"
+version = "6.0.0"
+description = "An open-source, interactive data visualization library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "plotly-6.0.0-py3-none-any.whl", hash = "sha256:f708871c3a9349a68791ff943a5781b1ec04de7769ea69068adcd9202e57653a"},
+ {file = "plotly-6.0.0.tar.gz", hash = "sha256:c4aad38b8c3d65e4a5e7dd308b084143b9025c2cc9d5317fc1f1d30958db87d3"},
+]
+
+[package.dependencies]
+narwhals = ">=1.15.1"
+packaging = "*"
+
+[package.extras]
+express = ["numpy"]
+
[[package]]
name = "pluggy"
version = "1.5.0"
@@ -869,6 +913,24 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
+[[package]]
+name = "progressbar2"
+version = "4.5.0"
+description = "A Python Progressbar library to provide visual (yet text based) progress to long running operations."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "progressbar2-4.5.0-py3-none-any.whl", hash = "sha256:625c94a54e63915b3959355e6d4aacd63a00219e5f3e2b12181b76867bf6f628"},
+ {file = "progressbar2-4.5.0.tar.gz", hash = "sha256:6662cb624886ed31eb94daf61e27583b5144ebc7383a17bae076f8f4f59088fb"},
+]
+
+[package.dependencies]
+python-utils = ">=3.8.1"
+
+[package.extras]
+docs = ["sphinx (>=1.8.5)", "sphinx-autodoc-typehints (>=1.6.0)"]
+tests = ["dill (>=0.3.6)", "flake8 (>=3.7.7)", "freezegun (>=0.3.11)", "pytest (>=4.6.9)", "pytest-cov (>=2.6.1)", "pytest-mypy", "pywin32", "sphinx (>=1.8.5)"]
+
[[package]]
name = "pycodestyle"
version = "2.12.1"
@@ -977,6 +1039,25 @@ files = [
[package.dependencies]
six = ">=1.5"
+[[package]]
+name = "python-utils"
+version = "3.9.1"
+description = "Python Utils is a module with some convenient utilities not included with the standard Python install"
+optional = false
+python-versions = ">=3.9.0"
+files = [
+ {file = "python_utils-3.9.1-py2.py3-none-any.whl", hash = "sha256:0273d7363c7ad4b70999b2791d5ba6b55333d6f7a4e4c8b6b39fb82b5fab4613"},
+ {file = "python_utils-3.9.1.tar.gz", hash = "sha256:eb574b4292415eb230f094cbf50ab5ef36e3579b8f09e9f2ba74af70891449a0"},
+]
+
+[package.dependencies]
+typing_extensions = ">3.10.0.2"
+
+[package.extras]
+docs = ["mock", "python-utils", "sphinx"]
+loguru = ["loguru"]
+tests = ["blessings", "loguru", "loguru-mypy", "mypy-ipython", "pyright", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mypy", "ruff", "sphinx", "types-setuptools"]
+
[[package]]
name = "pytrade"
version = "0.1.0"
@@ -1224,4 +1305,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "04e1fdcc5d2c4cd28ed0b8a4c2965cb10ed8de054d32d92915de51710b706bdb"
+content-hash = "7598843080ad7b5a656df7873bbe96d6a1162cefc58f6aad9e2785fc63cc20f0"
diff --git a/pyproject.toml b/pyproject.toml
index b004115..447917c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,6 +11,8 @@ pytrade = {path = "lib/PyTrade", develop = true}
pandas = "^2.2.2"
pytest-asyncio = "^0.25.1"
autoflake = "^2.3.1"
+plotly = "^6.0.0"
+progressbar2 = "^4.5.0"
[tool.poetry.group.dev.dependencies]
@@ -52,4 +54,8 @@ omit = [
[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]
-pythonpath = [ "." ]
\ No newline at end of file
+pythonpath = [ "." ]
+
+[[tool.mypy.overrides]]
+module = ["plotly.*"]
+ignore_missing_imports = true
\ No newline at end of file
diff --git a/pytradebacktest/backtest.py b/pytradebacktest/backtest.py
index e1c9303..5c490ca 100644
--- a/pytradebacktest/backtest.py
+++ b/pytradebacktest/backtest.py
@@ -1,11 +1,14 @@
from typing import Type
import pandas as pd
+from progressbar import ProgressBar
from pytrade.indicator import Indicator
+from pytrade.instruments import Granularity
from pytrade.strategy import FxStrategy
from pytradebacktest.broker import BacktestBroker
from pytradebacktest.data import MarketData
+from pytradebacktest.plot import plot
from pytradebacktest.stats import Stats
@@ -38,24 +41,38 @@ def increment_indicator(self):
Indicator._update = increment_indicator
- broker = BacktestBroker(self.data, self.cash, self.comission, self.margin)
+ self.broker = BacktestBroker(self.data, self.cash, self.comission, self.margin)
- strategy = self.kstrategy(broker, self.data)
+ strategy = self.kstrategy(self.broker, self.data)
strategy.init()
- while self.data.next():
+ with ProgressBar(max_value=len(self.data), redirect_stdout=True) as bar:
+ while self.data.next():
- broker.next()
- strategy.next()
+ self.broker.next()
+ strategy.next()
+
+ bar.next()
# Close any open trades:
- broker.close_trades()
+ self.broker.close_trades()
# Call broker one last time to clean up any outstanding orders from strategy
- broker.next()
+ self.broker.next()
# Claculate results/stats
- equity = pd.Series(broker._equity).bfill().fillna(broker._cash).values
- return Stats(broker.closed_trades, equity, self.data, strategy)
+ equity = pd.Series(self.broker._equity).bfill().fillna(self.broker._cash).values
+ return Stats(self.broker.closed_trades, equity, self.data, strategy)
def plot(self):
- pass
+ instruments = set([t.instrument for t in self.broker.closed_trades])
+ for instrument in instruments:
+ goog_data = self.data.get(instrument, Granularity.M1)
+ trades = [
+ trade
+ for trade in self.broker.closed_trades
+ if trade.instrument == instrument
+ ]
+ equity_df = pd.DataFrame(
+ self.broker._equity, index=self.data._market_index, columns=["Equity"]
+ )
+ plot(goog_data, equity_df, trades)
diff --git a/pytradebacktest/broker.py b/pytradebacktest/broker.py
index 6a8845a..6f0ae57 100644
--- a/pytradebacktest/broker.py
+++ b/pytradebacktest/broker.py
@@ -169,7 +169,8 @@ def _process_contingent_order(self, ctx: OrderContext):
closed = self._reduce_trade(
trade, _order_size, ctx.entry_price, ctx.entry_time
)
- if closed:
+ # IF we closed, it removes SL and TP
+ if not closed:
self.orders.remove(order)
def _process_market_order(self, ctx: OrderContext):
@@ -280,9 +281,9 @@ def close_trades(self):
def _close_trade(self, trade: Trade, price: float, timestamp: Timestamp):
self.trades.remove(trade)
- if trade.sl:
+ if trade.sl is not None:
self.orders.remove(trade.sl)
- if trade.tp:
+ if trade.tp is not None:
self.orders.remove(trade.tp)
trade.close(price, timestamp)
diff --git a/pytradebacktest/data.py b/pytradebacktest/data.py
index 8f460ea..78c3009 100644
--- a/pytradebacktest/data.py
+++ b/pytradebacktest/data.py
@@ -102,8 +102,8 @@ def load(self) -> list[InstrumentData]:
_sources = []
for source in self.sources:
- df = load_csv(source.path, parse_dates=["Timestamp"])
- df = df.set_index("Timestamp")
+ df = load_csv(source.path, parse_dates=["datetime"])
+ df = df.set_index("datetime")
df.replace("", np.nan, inplace=True)
df.dropna(inplace=True)
instrument_data = InstrumentData(source.instrument, source.granularity, df)
diff --git a/pytradebacktest/main.py b/pytradebacktest/main.py
new file mode 100644
index 0000000..e69de29
diff --git a/pytradebacktest/plot.py b/pytradebacktest/plot.py
new file mode 100644
index 0000000..47be6f2
--- /dev/null
+++ b/pytradebacktest/plot.py
@@ -0,0 +1,105 @@
+import pandas as pd
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from pytrade.interfaces.data import IInstrumentData
+from pytrade.models import Trade
+
+# from pytradebacktest.data import MarketData
+
+
+def plot(data: IInstrumentData, equity: pd.DataFrame, trades: list[Trade]):
+
+ fig = make_subplots(
+ rows=2, cols=1, row_heights=[0.2, 0.8], subplot_titles=("Equity", "Trades")
+ )
+
+ _plot_equity(fig, equity)
+ _plot_ohlc(fig, data)
+ _plot_trades(fig, data, trades)
+
+ fig.show()
+
+
+def _plot_equity(fig: go.Figure, equity: pd.DataFrame):
+ fig.add_trace(
+ go.Scatter(x=equity.index, y=equity["Equity"], mode="lines", name="Equity"),
+ row=1,
+ col=1,
+ )
+
+
+def _plot_ohlc(fig: go.Figure, data: IInstrumentData):
+ df = data.df
+ ohlc = go.Candlestick(
+ x=df.index, open=df["open"], high=df["high"], low=df["low"], close=df["close"]
+ )
+
+ fig.add_trace(ohlc, row=2, col=1)
+ fig.update_layout(xaxis2_rangeslider_visible=False)
+
+
+def _plot_trades(fig: go.Figure, data: IInstrumentData, trades: list[Trade]):
+ for trade in trades:
+ # Add entry points
+ fig.add_trace(
+ go.Scatter(
+ x=[trade.entry_time],
+ y=[trade.entry_price],
+ mode="markers",
+ marker=dict(
+ size=10,
+ symbol="triangle-up" if trade.is_long else "triangle-down",
+ color="green" if trade.is_long else "red",
+ line=dict(color="black", width=1),
+ ),
+ hovertemplate=f"""
+Date:%{{x}}
+Entry:%{{y}}
+Exit:{trade.exit_price}
+Size:{trade.size}
+{("SL: {trade.sl.stop}
" if trade.sl is not None else "")}
+{("TP: {trade.tp.limit}
" if trade.tp is not None else "")}
+P/L:{trade.pl}
+""",
+ ),
+ row=2,
+ col=1,
+ )
+
+ # Add line to exit
+ fig.add_trace(
+ go.Scatter(
+ x=[trade.entry_time, trade.exit_time],
+ y=[trade.entry_price, trade.exit_price],
+ mode="lines",
+ line=dict(color="green" if trade.pl > 0 else "red"),
+ ),
+ row=2,
+ col=1,
+ )
+
+ if trade.sl is not None:
+ # Add stop loss line
+ fig.add_trace(
+ go.Scatter(
+ x=[trade.entry_time, trade.exit_time],
+ y=[trade.sl.stop, trade.sl.stop],
+ mode="lines",
+ line=dict(color="red", dash="dashdot"),
+ ),
+ row=2,
+ col=1,
+ )
+
+ if trade.tp is not None:
+ # Add take profit line
+ fig.add_trace(
+ go.Scatter(
+ x=[trade.entry_time, trade.exit_time],
+ y=[trade.tp.limit, trade.tp.limit],
+ mode="lines",
+ line=dict(color="green", dash="dashdot"),
+ ),
+ row=2,
+ col=1,
+ )
diff --git a/pytradebacktest/stats.py b/pytradebacktest/stats.py
index bb1a0b5..240ad6a 100644
--- a/pytradebacktest/stats.py
+++ b/pytradebacktest/stats.py
@@ -250,15 +250,15 @@ def win_rate(self):
@property
def best_trade_return(self):
- return self.returns.max() * 100
+ return self.returns.max()
@property
def worst_trade_return(self):
- return self.returns.min() * 100
+ return self.returns.min()
@property
def avg_trade_return(self):
- return self._geometric_mean(self.returns) * 100
+ return self._geometric_mean(self.returns/100) * 100
@property
def max_trade_duration(self):
@@ -275,7 +275,7 @@ def profit_factor(self):
@property
def expectancy(self):
- return self.returns.mean() * 100
+ return self.returns.mean()
@property
def SQN(self):
diff --git a/tests/unit/assets/EURGBP-2024-05_1Min.csv b/tests/unit/assets/EURGBP-2024-05_1Min.csv
index 6106104..dd9decf 100644
--- a/tests/unit/assets/EURGBP-2024-05_1Min.csv
+++ b/tests/unit/assets/EURGBP-2024-05_1Min.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close
+datetime,open,high,low,close
2024-05-01 00:00:00,0.85401,0.85405,0.85397,0.85403
2024-05-01 00:01:00,0.85402,0.85404,0.85392,0.85403
2024-05-01 00:02:00,0.85397,0.85403,0.85396,0.854
diff --git a/tests/unit/assets/EURGBP-2024-05_5Min.csv b/tests/unit/assets/EURGBP-2024-05_5Min.csv
index 802923c..f92941d 100644
--- a/tests/unit/assets/EURGBP-2024-05_5Min.csv
+++ b/tests/unit/assets/EURGBP-2024-05_5Min.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close
+datetime,open,high,low,close
2024-05-01 00:00:00,0.85401,0.85405,0.85391,0.85396
2024-05-01 00:05:00,0.85396,0.85397,0.85389,0.85392
2024-05-01 00:10:00,0.85391,0.85397,0.85386,0.85396
diff --git a/tests/unit/assets/EURJPY-2024-05_1Min.csv b/tests/unit/assets/EURJPY-2024-05_1Min.csv
index 0b4286b..5a2accc 100644
--- a/tests/unit/assets/EURJPY-2024-05_1Min.csv
+++ b/tests/unit/assets/EURJPY-2024-05_1Min.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close
+datetime,open,high,low,close
2024-05-01 00:00:00,168.263,168.274,168.201,168.229
2024-05-01 00:01:00,168.247,168.261,168.217,168.222
2024-05-01 00:02:00,168.222,168.242,168.191,168.213
diff --git a/tests/unit/assets/EURJPY-2024-05_5Min.csv b/tests/unit/assets/EURJPY-2024-05_5Min.csv
index 681c392..4cafad5 100644
--- a/tests/unit/assets/EURJPY-2024-05_5Min.csv
+++ b/tests/unit/assets/EURJPY-2024-05_5Min.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close
+datetime,open,high,low,close
2024-05-01 00:00:00,168.263,168.274,168.177,168.226
2024-05-01 00:05:00,168.24,168.285,168.2,168.256
2024-05-01 00:10:00,168.274,168.275,168.201,168.242
diff --git a/tests/unit/assets/EURUSD-2024-05_1Min.csv b/tests/unit/assets/EURUSD-2024-05_1Min.csv
index b0a3cae..f6a08f4 100644
--- a/tests/unit/assets/EURUSD-2024-05_1Min.csv
+++ b/tests/unit/assets/EURUSD-2024-05_1Min.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close
+datetime,open,high,low,close
2024-05-01 00:00:00,1.06657,1.06672,1.06655,1.0667
2024-05-01 00:01:00,1.06665,1.06672,1.06655,1.06663
2024-05-01 00:02:00,1.06655,1.06666,1.06655,1.06663
diff --git a/tests/unit/assets/EURUSD-2024-05_5Min.csv b/tests/unit/assets/EURUSD-2024-05_5Min.csv
index 93b2b42..c1c931a 100644
--- a/tests/unit/assets/EURUSD-2024-05_5Min.csv
+++ b/tests/unit/assets/EURUSD-2024-05_5Min.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close
+datetime,open,high,low,close
2024-05-01 00:00:00,1.06657,1.06672,1.06647,1.0667
2024-05-01 00:05:00,1.06666,1.06671,1.06652,1.06663
2024-05-01 00:10:00,1.06656,1.06665,1.06633,1.06643
diff --git a/tests/unit/assets/GBPUSD-2024-05_1Min.csv b/tests/unit/assets/GBPUSD-2024-05_1Min.csv
index 0acc55d..6f1f3a4 100644
--- a/tests/unit/assets/GBPUSD-2024-05_1Min.csv
+++ b/tests/unit/assets/GBPUSD-2024-05_1Min.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close
+datetime,open,high,low,close
2024-05-01 00:00:00,1.24883,1.24896,1.24878,1.24893
2024-05-01 00:01:00,1.24894,1.24904,1.24883,1.24888
2024-05-01 00:02:00,1.24884,1.24894,1.24884,1.24889
diff --git a/tests/unit/assets/GBPUSD-2024-05_5Min.csv b/tests/unit/assets/GBPUSD-2024-05_5Min.csv
index c2b25c4..00ef5ae 100644
--- a/tests/unit/assets/GBPUSD-2024-05_5Min.csv
+++ b/tests/unit/assets/GBPUSD-2024-05_5Min.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close
+datetime,open,high,low,close
2024-05-01 00:00:00,1.24883,1.24906,1.24878,1.24901
2024-05-01 00:05:00,1.24904,1.24911,1.24891,1.24902
2024-05-01 00:10:00,1.24903,1.24906,1.24868,1.24872
diff --git a/tests/unit/assets/GOOG.csv b/tests/unit/assets/GOOG.csv
index eed8d1c..f44bddd 100644
--- a/tests/unit/assets/GOOG.csv
+++ b/tests/unit/assets/GOOG.csv
@@ -1,4 +1,4 @@
-Timestamp,Open,High,Low,Close,Volume
+datetime,open,high,low,close,volume
2004-08-19,100,104.06,95.96,100.34,22351900
2004-08-20,101.01,109.08,100.5,108.31,11428600
2004-08-23,110.75,113.48,109.05,109.4,9137200
diff --git a/tests/unit/test_backtest_broker.py b/tests/unit/test_backtest_broker.py
index 942bcbc..6d0f5b8 100644
--- a/tests/unit/test_backtest_broker.py
+++ b/tests/unit/test_backtest_broker.py
@@ -23,7 +23,7 @@ def test_fill_market_order(iterations, test_stock_universe: MarketData):
assert len(broker.orders) == 0
assert len(broker.trades) == 1
trade = broker.trades[0]
- assert trade.entry_price == goog_data.Open.iloc[iterations - 1]
+ assert trade.entry_price == goog_data.open.iloc[iterations - 1]
def test_market_order_not_enough_equity(test_stock_universe: MarketData):
@@ -45,12 +45,12 @@ def test_stop_order_conversion(price_index, buy, test_stock_universe: MarketData
broker = BacktestBroker(test_stock_universe, 100000, 0, 1, False, False, False)
price_timestamp = (
- goog_data[:price_index].High.idxmax()
+ goog_data[:price_index].high.idxmax()
if buy
- else goog_data[:price_index].Low.idxmin()
+ else goog_data[:price_index].low.idxmin()
)
price_idx = goog_data.index.get_loc(price_timestamp)
- price = goog_data.High.iloc[price_idx] if buy else goog_data.Low.iloc[price_idx]
+ price = goog_data.high.iloc[price_idx] if buy else goog_data.low.iloc[price_idx]
stop = price - 0.01 if buy else price + 0.01 # Set to 1 cent past price
size = 100 if buy else -100
broker.order(Order("GOOG", size, stop=stop))
@@ -80,12 +80,12 @@ def test_limit_order_conversion(price_index, buy, test_stock_universe: MarketDat
broker = BacktestBroker(test_stock_universe, 100000, 0, 1, False, False, False)
price_timestamp = (
- goog_data[:price_index].Low.idxmin()
+ goog_data[:price_index].low.idxmin()
if buy
- else goog_data[:price_index].High.idxmax()
+ else goog_data[:price_index].high.idxmax()
)
price_idx = goog_data.index.get_loc(price_timestamp)
- price = goog_data.Low.iloc[price_idx] if buy else goog_data.High.iloc[price_idx]
+ price = goog_data.low.iloc[price_idx] if buy else goog_data.high.iloc[price_idx]
limit = price + 0.01 if buy else price - 0.01 # Set to 1 cent past price
size = 100 if buy else -100
broker.order(Order("GOOG", size, limit=limit))
@@ -115,16 +115,16 @@ def test_stop_loss_order(index, buy, test_stock_universe: MarketData):
broker = BacktestBroker(test_stock_universe, 100000, 0, 1, False, False, False)
stop_timestmap = (
- goog_data[index:].Low.idxmin() if buy else goog_data[index:].High.idxmax()
+ goog_data[index:].low.idxmin() if buy else goog_data[index:].high.idxmax()
)
stop_idx = goog_data.index.get_loc(stop_timestmap)
stop_price = (
- goog_data.Low.iloc[stop_idx] + 0.01
+ goog_data.low.iloc[stop_idx] + 0.01
if buy
- else goog_data.High.iloc[stop_idx] - 0.01
+ else goog_data.high.iloc[stop_idx] - 0.01
)
size = 100 if buy else -100
- entry_price = goog_data.Open.iloc[index]
+ entry_price = goog_data.open.iloc[index]
for i in range(index):
test_stock_universe.next()
@@ -162,16 +162,16 @@ def test_take_profit_order(index, buy, test_stock_universe: MarketData):
broker = BacktestBroker(test_stock_universe, 100000, 0, 1, False, False, False)
limit_timestmap = (
- goog_data[index:].High.idxmax() if buy else goog_data[index:].Low.idxmin()
+ goog_data[index:].high.idxmax() if buy else goog_data[index:].low.idxmin()
)
limit_idx = goog_data.index.get_loc(limit_timestmap)
limit_price = (
- goog_data.High.iloc[limit_idx] - 0.01
+ goog_data.high.iloc[limit_idx] - 0.01
if buy
- else goog_data.Low.iloc[limit_idx] + 0.01
+ else goog_data.low.iloc[limit_idx] + 0.01
)
size = 100 if buy else -100
- entry_price = goog_data.Open.iloc[index]
+ entry_price = goog_data.open.iloc[index]
for i in range(index):
test_stock_universe.next()
@@ -254,6 +254,56 @@ def test_change_position(test_stock_universe: MarketData):
assert len(goog_position.trades) == 1
+def test_close_and_open_oppposite_position(test_stock_universe: MarketData):
+ broker = BacktestBroker(test_stock_universe, 10000, 0, 1, False, False, False)
+ goog_position = broker.get_position("GOOG")
+
+ broker.order(Order("GOOG", 100))
+ test_stock_universe.next()
+ broker.next()
+
+ assert len(broker.orders) == 0
+ assert len(broker.trades) == 1
+
+ assert goog_position.is_long is True
+ assert goog_position.size == 100
+ assert len(goog_position.trades) == 1
+
+ broker.close_position("GOOG")
+ broker.order(Order("GOOG", -100))
+ test_stock_universe.next()
+ broker.next()
+
+ assert goog_position.is_long is False
+ assert goog_position.size == -100
+ assert len(goog_position.trades) == 1
+
+
+def test_close_and_open_oppposite_position_with_sl_tp(test_stock_universe: MarketData):
+ broker = BacktestBroker(test_stock_universe, 10000, 0, 1, False, False, False)
+ goog_position = broker.get_position("GOOG")
+
+ broker.order(Order("GOOG", 100, stop_loss_on_fill=80, take_profit_on_fill=140))
+ test_stock_universe.next()
+ broker.next()
+
+ assert len(broker.orders) == 2
+ assert len(broker.trades) == 1
+
+ assert goog_position.is_long is True
+ assert goog_position.size == 100
+ assert len(goog_position.trades) == 1
+
+ broker.close_position("GOOG")
+ broker.order(Order("GOOG", -100))
+ test_stock_universe.next()
+ broker.next()
+
+ assert goog_position.is_long is False
+ assert goog_position.size == -100
+ assert len(goog_position.trades) == 1
+
+
def test_reduce_position(test_stock_universe: MarketData):
broker = BacktestBroker(test_stock_universe, 10000, 0, 1, False, False, False)
goog_position = broker.get_position("GOOG")
diff --git a/tests/unit/test_backtest_indicator.py b/tests/unit/test_backtest_indicator.py
index ee53609..9999fa4 100644
--- a/tests/unit/test_backtest_indicator.py
+++ b/tests/unit/test_backtest_indicator.py
@@ -8,7 +8,7 @@
class OpenIndicator(Indicator):
def _run(self, *args, **kwargs):
- return self._data.df.Open
+ return self._data.df.open
def test_indicator_values(data: np.ndarray, indicator: Indicator):
diff --git a/tests/unit/test_backtest_plot.py b/tests/unit/test_backtest_plot.py
new file mode 100644
index 0000000..5a45713
--- /dev/null
+++ b/tests/unit/test_backtest_plot.py
@@ -0,0 +1,12 @@
+import pytest
+
+# from pytradebacktest.backtest import Backtest
+# from tests.unit.resources.indicators import SmaCross
+
+
+@pytest.mark.asyncio
+async def test_plot_ohlc(test_stock_universe):
+ # test = Backtest(test_stock_universe, SmaCross, 10000)
+ # stats = await test.run()
+
+ pass
diff --git a/tests/unit/test_backtest_strategy.py b/tests/unit/test_backtest_strategy.py
index 8ed81b7..9b74c53 100644
--- a/tests/unit/test_backtest_strategy.py
+++ b/tests/unit/test_backtest_strategy.py
@@ -1,9 +1,9 @@
from unittest.mock import patch
+import pandas as pd
import pytest
from pytrade.indicator import Indicator
from pytrade.instruments import CandleSubscription, FxInstrument, Granularity
-from pytrade.interfaces.data import IInstrumentData
from pytrade.strategy import FxStrategy
from pytradebacktest.broker import BacktestBroker
@@ -68,16 +68,16 @@ def increment_indicator(self):
strategy = BacktestStrategy(broker, test_fx_universe)
strategy.init()
- m1_data: IInstrumentData = test_fx_universe.get(
+ m1_data: pd.DataFrame = test_fx_universe.get(
BACKTEST_INSTRUMENT, Granularity.M1
).df.copy()
- m5_data: IInstrumentData = test_fx_universe.get(
+ m5_data: pd.DataFrame = test_fx_universe.get(
BACKTEST_INSTRUMENT, Granularity.M5
).df.copy()
indicator_data = {"eurusd_m1": m1_data, "eurusd_m5": m5_data}
expected_indicator_values = {
- "eurusd_m1": m1_data.Open > m1_data.Close,
- "eurusd_m5": m5_data.Open > m5_data.Close,
+ "eurusd_m1": m1_data.open > m1_data.close,
+ "eurusd_m5": m5_data.open > m5_data.close,
}
_indicators = {
@@ -119,16 +119,16 @@ def increment_indicator(self):
strategy = BacktestStrategy(broker, test_fx_universe)
strategy.init()
- test_data = test_fx_universe.get(
+ test_data: pd.DataFrame = test_fx_universe.get(
BACKTEST_INSTRUMENT, BACKTEST_GRANULARITY
).df.copy()
while test_fx_universe.next():
strategy.next()
- expected_buy_calls = test_data[test_data.Open <= test_data.Close].count().Open
- expected_sell_calls = test_data[test_data.Open > test_data.Close].count().Open
- assert expected_buy_calls + expected_sell_calls == test_data.count().Open
+ expected_buy_calls = test_data[test_data.open <= test_data.close].count().open
+ expected_sell_calls = test_data[test_data.open > test_data.close].count().open
+ assert expected_buy_calls + expected_sell_calls == test_data.count().open
assert mock_buy.call_count == expected_buy_calls
assert mock_sell.call_count == expected_sell_calls
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index b62ff86..564cddc 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -13,5 +13,5 @@
],
)
def test_load_csvs(path, count):
- df = load_csv(path, parse_dates=["Timestamp"])
+ df = load_csv(path, parse_dates=["datetime"])
assert df.shape[0] == count