Skip to content

Commit a004944

Browse files
authored
remove datetime and only use pandas Timestamp class (#19)
1 parent 0e270c8 commit a004944

File tree

12 files changed

+303
-241
lines changed

12 files changed

+303
-241
lines changed

.devcontainer/devcontainer.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "kissbt dev",
3+
"image": "mcr.microsoft.com/devcontainers/python:3.13",
4+
"postCreateCommand": "pip install -e .[dev]",
5+
"customizations": {
6+
"vscode": {
7+
"extensions": [
8+
"ms-python.python",
9+
"ms-python.vscode-pylance",
10+
"ms-toolsai.jupyter",
11+
"github.copilot",
12+
"github.copilot-chat",
13+
"charliermarsh.ruff",
14+
"ms-python.mypy-type-checker"
15+
],
16+
"settings": {
17+
"python.defaultInterpreterPath": "/usr/local/bin/python",
18+
"[python]": {
19+
"editor.defaultFormatter": "charliermarsh.ruff",
20+
"editor.formatOnSave": true,
21+
"editor.codeActionsOnSave": {
22+
"source.fixAll.ruff": "explicit",
23+
"source.organizeImports.ruff": "explicit"
24+
}
25+
},
26+
"ruff.enable": true,
27+
"ruff.organizeImports": true,
28+
"python.analysis.typeCheckingMode": "strict",
29+
"python.analysis.autoImportCompletions": true,
30+
"python.analysis.indexing": true,
31+
"mypy.enabled": true,
32+
"mypy.runUsingActiveInterpreter": true,
33+
"python.testing.pytestEnabled": true,
34+
"python.testing.unittestEnabled": false,
35+
"python.testing.pytestArgs": ["tests"],
36+
"files.exclude": {
37+
"**/__pycache__": true,
38+
"**/*.pyc": true,
39+
"**/.mypy_cache": true,
40+
"**/.ruff_cache": true
41+
}
42+
}
43+
}
44+
}
45+
}

examples/20241227_introduction.ipynb

Lines changed: 138 additions & 130 deletions
Large diffs are not rendered by default.

kissbt/analyzer.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ def __init__(self, broker: Broker, bar_size: str = "1D") -> None:
2323
Parameters:
2424
broker (Broker): The broker instance containing the trading history.
2525
bar_size (str): The time interval of each bar in the data, supported units
26-
are 'S' for seconds, 'M' for minutes, 'H' for hours and 'D' for days
26+
are 'S' for seconds, 'T' for minutes, 'H' for hours and 'D' for days
2727
(default is "1D").
2828
"""
2929

3030
value = int(bar_size[:-1])
3131
unit = bar_size[-1]
32-
seconds_multiplier = {"S": 1, "M": 60, "H": 3600, "D": 3600 * 6.5}
32+
seconds_multiplier = {"S": 1, "T": 60, "H": 3600, "D": 3600 * 6.5}
3333
if unit not in seconds_multiplier:
3434
raise ValueError(f"Unsupported bar size unit: {unit}")
3535
self.seconds_per_bar = value * seconds_multiplier[unit]
@@ -280,14 +280,14 @@ def plot_drawdowns(self, **kwargs: Dict[str, Any]) -> None:
280280
**kwargs(Dict[str, Any]): Additional keyword arguments to pass to the plot
281281
function of pandas.
282282
"""
283-
columns_to_plot = ["date", "drawdown"]
283+
columns_to_plot = ["timestamp", "drawdown"]
284284
if "benchmark_drawdown" in self.analysis_df.columns:
285285
columns_to_plot.append("benchmark_drawdown")
286286

287287
self.analysis_df.loc[:, columns_to_plot].plot(
288-
x="date",
288+
x="timestamp",
289289
title="Portfolio Drawdown Over Time",
290-
xlabel="Date",
290+
xlabel="Timestamp",
291291
ylabel="Drawdown %",
292292
**kwargs,
293293
)
@@ -299,24 +299,24 @@ def plot_equity_curve(self, logy: bool = False, **kwargs: Dict[str, Any]) -> Non
299299
This method creates a line plot showing the portfolio's cash, total value, and
300300
benchmark over time for comparison. If the benchmark is not available, it will
301301
plot only the available columns. If logy is True, the cash column will be
302-
excluded, to focus on the total value and benchmark on logaritmic scale.
302+
excluded, to focus on the total value and benchmark on logarithmic scale.
303303
304304
Parameters:
305305
logy (bool): If True, use a logarithmic scale for the y-axis and exclude the
306306
cash column.
307307
**kwargs(Dict[str, Any]): Additional keyword arguments to pass to the plot
308308
function of pandas.
309309
"""
310-
columns_to_plot = ["date", "total_value"]
310+
columns_to_plot = ["timestamp", "total_value"]
311311
if "benchmark" in self.analysis_df.columns:
312312
columns_to_plot.append("benchmark")
313313
if not logy:
314314
columns_to_plot.append("cash")
315315

316316
self.analysis_df.loc[:, columns_to_plot].plot(
317-
x="date",
317+
x="timestamp",
318318
title="Portfolio Equity Curve Over Time",
319-
xlabel="Date",
319+
xlabel="Timestamp",
320320
ylabel="Value",
321321
logy=logy,
322322
**kwargs,

kissbt/broker.py

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from datetime import datetime
2-
from typing import Dict, List, Optional, cast
1+
from typing import Dict, List, Optional
32

43
import pandas as pd
54

@@ -24,7 +23,7 @@ class Broker:
2423
Typical usage:
2524
broker = Broker(start_capital=100000, fees=0.001)
2625
broker.place_order(Order("AAPL", 100, OrderType.OPEN))
27-
broker.update(next_bar, next_datetime)
26+
broker.update(next_bar, next_timestamp)
2827
"""
2928

3029
def __init__(
@@ -64,9 +63,9 @@ def __init__(
6463
self._open_orders: List[Order] = []
6564

6665
self._current_bar: pd.DataFrame = pd.DataFrame()
67-
self._current_datetime: Optional[datetime] = None
66+
self._current_timestamp: Optional[pd.Timestamp] = None
6867
self._previous_bar: pd.DataFrame = pd.DataFrame()
69-
self._previous_datetime: Optional[datetime] = None
68+
self._previous_timestamp: Optional[pd.Timestamp] = None
7069

7170
self._long_only = long_only
7271
self._short_fee_rate = short_fee_rate
@@ -75,8 +74,8 @@ def __init__(
7574
self._benchmark = benchmark
7675
self._benchmark_size = 0.0
7776

78-
self._history: Dict[str, List[float]] = {
79-
"date": [],
77+
self._history: Dict[str, List[float | int | pd.Timestamp]] = {
78+
"timestamp": [],
8079
"cash": [],
8180
"long_position_value": [],
8281
"short_position_value": [],
@@ -90,7 +89,7 @@ def _update_history(self):
9089
"""
9190
Updates the history dictionary with the current portfolio state.
9291
"""
93-
self._history["date"].append(self._current_datetime)
92+
self._history["timestamp"].append(self._current_timestamp)
9493
self._history["cash"].append(self._cash)
9594
self._history["long_position_value"].append(self.long_position_value)
9695
self._history["short_position_value"].append(self.short_position_value)
@@ -164,7 +163,7 @@ def _get_price_for_order(self, order: Order, bar: pd.DataFrame) -> float | None:
164163
raise ValueError(f"Unknown order type {order.order_type}")
165164

166165
def _update_closed_positions(
167-
self, ticker: str, size: float, price: float, datetime: datetime
166+
self, ticker: str, size: float, price: float, timestamp: pd.Timestamp
168167
):
169168
"""
170169
Updates the list of closed positions for a given trade.
@@ -178,7 +177,7 @@ def _update_closed_positions(
178177
ticker (str): The ticker symbol of the position
179178
size (float): Position size (positive for long, negative for short)
180179
price (float): The current closing/reduction price
181-
datetime (datetime): Timestamp of the closing/reduction
180+
timestamp (timestamp): Timestamp of the closing/reduction
182181
"""
183182
if (
184183
ticker in self._open_positions
@@ -191,9 +190,9 @@ def _update_closed_positions(
191190
self._open_positions[ticker].ticker,
192191
min(self._open_positions[ticker].size, abs(size)),
193192
self._open_positions[ticker].price,
194-
self._open_positions[ticker].datetime,
193+
self._open_positions[ticker].timestamp,
195194
price,
196-
datetime,
195+
timestamp,
197196
),
198197
)
199198
# if short position is closed/reduced
@@ -203,59 +202,60 @@ def _update_closed_positions(
203202
self._open_positions[ticker].ticker,
204203
max(self._open_positions[ticker].size, -size),
205204
price,
206-
datetime,
205+
timestamp,
207206
self._open_positions[ticker].price,
208-
self._open_positions[ticker].datetime,
207+
self._open_positions[ticker].timestamp,
209208
),
210209
)
211210

212211
def _update_open_positions(
213-
self, ticker: str, size: float, price: float, datetime: datetime
212+
self, ticker: str, size: float, price: float, timestamp: pd.Timestamp
214213
):
215214
"""
216215
Updates the open positions for a given ticker.
217216
218217
If the ticker already exists in the open positions, it updates the size, price,
219-
and datetime based on the new transaction. If the size of the position becomes
218+
and timestamp based on the new transaction. If the size of the position becomes
220219
zero, the position is removed. If the ticker does not exist, a new open position
221220
is created.
222221
223222
Args:
224223
ticker (str): The ticker symbol of the asset.
225224
size (float): The size of the position.
226225
price (float): The price at which the position was opened or updated.
227-
datetime (datetime): The datetime when the position was opened or updated.
226+
timestamp (Timestamp): The timestamp when the position was opened or
227+
updated.
228228
"""
229229
if ticker in self._open_positions:
230230
if size + self._open_positions[ticker].size == 0.0:
231231
self._open_positions.pop(ticker)
232232
else:
233233
open_position_size = self._open_positions[ticker].size + size
234234
open_position_price = price
235-
open_position_datetime = datetime
235+
open_position_timestamp = timestamp
236236

237237
if size * self._open_positions[ticker].size > 0.0:
238238
open_position_price = (
239239
self._open_positions[ticker].size
240240
* self._open_positions[ticker].price
241241
+ size * price
242242
) / (self._open_positions[ticker].size + size)
243-
open_position_datetime = self._open_positions[ticker].datetime
243+
open_position_timestamp = self._open_positions[ticker].timestamp
244244
elif abs(self._open_positions[ticker].size) > abs(size):
245-
open_position_datetime = self._open_positions[ticker].datetime
245+
open_position_timestamp = self._open_positions[ticker].timestamp
246246
open_position_price = self._open_positions[ticker].price
247247
self._open_positions[ticker] = OpenPosition(
248248
ticker,
249249
open_position_size,
250250
open_position_price,
251-
open_position_datetime,
251+
open_position_timestamp,
252252
)
253253
else:
254254
self._open_positions[ticker] = OpenPosition(
255255
ticker,
256256
size,
257257
price,
258-
datetime,
258+
timestamp,
259259
)
260260

261261
def _update_cash(self, order: Order, price: float):
@@ -272,29 +272,29 @@ def _update_cash(self, order: Order, price: float):
272272
else:
273273
self._cash -= order.size * price * (1.0 - self._fees)
274274

275-
def _check_long_only_condition(self, order: Order, datetime: datetime):
275+
def _check_long_only_condition(self, order: Order, timestamp: pd.Timestamp):
276276
size = order.size
277277
if order.ticker in self._open_positions:
278278
size += self._open_positions[order.ticker].size
279279

280280
if size < 0.0:
281281
raise ValueError(
282-
f"Short selling is not allowed for {order.ticker} on {datetime}."
282+
f"Short selling is not allowed for {order.ticker} on {timestamp}."
283283
)
284284

285285
def _execute_order(
286286
self,
287287
order: Order,
288288
bar: pd.DataFrame,
289-
datetime: datetime,
289+
timestamp: pd.Timestamp,
290290
) -> bool:
291291
"""
292-
Executes an order based on the provided bar data and datetime.
292+
Executes an order based on the provided bar data and timestamp.
293293
294294
Args:
295295
order (Order): The order to be executed.
296296
bar (pd.DataFrame): The bar data containing price information.
297-
datetime (datetime): The datetime at which the order is executed.
297+
timestamp (Timestamp): The timestamp at which the order is executed.
298298
299299
Returns:
300300
bool: True if the order was successfully executed, False otherwise.
@@ -308,7 +308,7 @@ def _execute_order(
308308
return False
309309

310310
if self._long_only:
311-
self._check_long_only_condition(order, datetime)
311+
self._check_long_only_condition(order, timestamp)
312312

313313
price = self._get_price_for_order(order, bar)
314314

@@ -319,16 +319,16 @@ def _execute_order(
319319
# update cash for long and short positions
320320
self._update_cash(order, price)
321321

322-
self._update_closed_positions(ticker, order.size, price, datetime)
322+
self._update_closed_positions(ticker, order.size, price, timestamp)
323323

324-
self._update_open_positions(ticker, order.size, price, datetime)
324+
self._update_open_positions(ticker, order.size, price, timestamp)
325325

326326
return True
327327

328328
def update(
329329
self,
330330
next_bar: pd.DataFrame,
331-
next_datetime: pd.Timestamp,
331+
next_timestamp: pd.Timestamp,
332332
):
333333
"""
334334
Updates the broker's state with the next trading bar and executes pending
@@ -344,7 +344,7 @@ def update(
344344
Args:
345345
next_bar (pd.DataFrame): The next trading bar data containing at minimum
346346
'close' prices for assets
347-
next_datetime (pd.Timestamp): The timestamp for the next trading bar
347+
next_timestamp (pd.Timestamp): The timestamp for the next trading bar
348348
349349
Notes:
350350
- Short fees are calculated using the current bar's closing price
@@ -354,9 +354,9 @@ def update(
354354
- Good-till-cancel orders that aren't filled are retained for the next bar
355355
"""
356356
self._previous_bar = self._current_bar
357-
self._previous_datetime = self._current_datetime
357+
self._previous_timestamp = self._current_timestamp
358358
self._current_bar = next_bar
359-
self._current_datetime = next_datetime
359+
self._current_timestamp = next_timestamp
360360

361361
# consider short fees
362362
if not self._long_only:
@@ -384,7 +384,7 @@ def update(
384384
self._execute_order(
385385
Order(ticker, -self._open_positions[ticker].size, OrderType.CLOSE),
386386
self._previous_bar,
387-
cast(datetime, self._previous_datetime),
387+
self._previous_timestamp,
388388
)
389389

390390
# buy and sell assets
@@ -396,18 +396,18 @@ def update(
396396
if open_order.ticker in ticker_not_available:
397397
if open_order.size > 0:
398398
print(
399-
f"{open_order.ticker} could not be bought on {self._current_datetime}." # noqa: E501
399+
f"{open_order.ticker} could not be bought on {self._current_timestamp}." # noqa: E501
400400
)
401401
else:
402402
print(
403-
f"{open_order.ticker} could not be sold on {self._current_datetime}." # noqa: E501
403+
f"{open_order.ticker} could not be sold on {self._current_timestamp}." # noqa: E501
404404
)
405405
continue
406406
if (
407407
not self._execute_order(
408408
open_order,
409409
self._current_bar,
410-
cast(datetime, self._current_datetime),
410+
self._current_timestamp,
411411
)
412412
and open_order.good_till_cancel
413413
):
@@ -439,7 +439,7 @@ def liquidate_positions(self):
439439
self._execute_order(
440440
Order(ticker, -self._open_positions[ticker].size, OrderType.CLOSE),
441441
self._current_bar,
442-
self._current_datetime,
442+
self._current_timestamp,
443443
)
444444

445445
def place_order(self, order: Order):
@@ -524,7 +524,7 @@ def open_positions(self) -> Dict[str, OpenPosition]:
524524
- ticker: Financial instrument identifier
525525
- size: Position size (positive=long, negative=short)
526526
- price: Average entry price
527-
- datetime: Position opening timestamp
527+
- timestamp: Position opening timestamp
528528
529529
Returns:
530530
Dict[str, OpenPosition]: Dictionary mapping ticker symbols to positions.

0 commit comments

Comments
 (0)