Skip to content
Merged
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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
2 changes: 1 addition & 1 deletion lib/PyTrade
83 changes: 82 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -52,4 +54,8 @@ omit = [

[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]
pythonpath = [ "." ]
pythonpath = [ "." ]

[[tool.mypy.overrides]]
module = ["plotly.*"]
ignore_missing_imports = true
37 changes: 27 additions & 10 deletions pytradebacktest/backtest.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)
7 changes: 4 additions & 3 deletions pytradebacktest/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pytradebacktest/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Empty file added pytradebacktest/main.py
Empty file.
105 changes: 105 additions & 0 deletions pytradebacktest/plot.py
Original file line number Diff line number Diff line change
@@ -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}}<br>
Entry:%{{y}}<br>
Exit:{trade.exit_price}<br>
Size:{trade.size}<br>
{("SL: {trade.sl.stop}<br>" if trade.sl is not None else "")}
{("TP: {trade.tp.limit}<br>" 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,
)
8 changes: 4 additions & 4 deletions pytradebacktest/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/assets/EURGBP-2024-05_1Min.csv
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/assets/EURGBP-2024-05_5Min.csv
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/assets/EURJPY-2024-05_1Min.csv
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading