kissbt 0.1.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- kissbt/__init__.py +0 -0
- kissbt/analyzer.py +323 -0
- kissbt/broker.py +590 -0
- kissbt/engine.py +29 -0
- kissbt/entities.py +70 -0
- kissbt/strategy.py +98 -0
- kissbt-0.1.1.dist-info/LICENSE +201 -0
- kissbt-0.1.1.dist-info/METADATA +346 -0
- kissbt-0.1.1.dist-info/RECORD +11 -0
- kissbt-0.1.1.dist-info/WHEEL +5 -0
- kissbt-0.1.1.dist-info/top_level.txt +1 -0
kissbt/broker.py
ADDED
@@ -0,0 +1,590 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from typing import Dict, List, Optional
|
3
|
+
|
4
|
+
import pandas as pd
|
5
|
+
|
6
|
+
from kissbt.entities import ClosedPosition, OpenPosition, Order, OrderType
|
7
|
+
|
8
|
+
|
9
|
+
class Broker:
|
10
|
+
"""
|
11
|
+
Manages the portfolio's cash, open and closed positions, and pending orders.
|
12
|
+
|
13
|
+
This broker class is responsible for tracking capital, applying trading fees, and
|
14
|
+
handling order execution for both opening and closing positions. It maintains
|
15
|
+
consistency between current and previous market data bars, ensuring correct updates
|
16
|
+
for position values, cash, and any accrued expenses such as short fees.
|
17
|
+
|
18
|
+
The broker interfaces with several other components:
|
19
|
+
- Manages OpenPosition and ClosedPosition objects to track trade lifecycles
|
20
|
+
- Processes Order objects from Strategy class based on market data
|
21
|
+
- Records transaction history for analysis by Analyzer
|
22
|
+
- Receives updates from Engine for each market data bar
|
23
|
+
|
24
|
+
Typical usage:
|
25
|
+
broker = Broker(start_capital=100000, fees=0.001)
|
26
|
+
broker.place_order(Order("AAPL", 100, OrderType.OPEN))
|
27
|
+
broker.update(next_bar, next_datetime)
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
start_capital: float = 100000,
|
33
|
+
fees: float = 0.001,
|
34
|
+
long_only: bool = True,
|
35
|
+
short_fee_rate: float = 0.0050,
|
36
|
+
benchmark: Optional[str] = None,
|
37
|
+
):
|
38
|
+
"""
|
39
|
+
Initialize the Broker.
|
40
|
+
|
41
|
+
Sets up the initial capital, commission fees, and optional benchmark tracking.
|
42
|
+
Configures whether broker operates in long-only mode or allows short selling.
|
43
|
+
Initializes all internal data structures for managing positions, orders, and
|
44
|
+
history.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
start_capital: Initial amount of cash available for trading, defaults to
|
48
|
+
100000.
|
49
|
+
fees: Commission or transaction fee applied to each trade, defaults to
|
50
|
+
0.0050.
|
51
|
+
long_only: If True, restricts trading to long positions only.
|
52
|
+
Defaults to True.
|
53
|
+
short_fee_rate: Annual fee rate for maintaining short positions.
|
54
|
+
Defaults to 0.0050. Daily rate derived automatically.
|
55
|
+
benchmark: Symbol used for performance comparison.
|
56
|
+
If provided, broker tracks performance against this symbol.
|
57
|
+
"""
|
58
|
+
self._cash = start_capital
|
59
|
+
self._start_capital = start_capital
|
60
|
+
self._fees = fees
|
61
|
+
|
62
|
+
self._open_positions: Dict[str, OpenPosition] = dict()
|
63
|
+
self._closed_positions: List[ClosedPosition] = []
|
64
|
+
self._open_orders: List[Order] = []
|
65
|
+
|
66
|
+
self._current_bar: pd.DataFrame = pd.DataFrame()
|
67
|
+
self._current_datetime = None
|
68
|
+
self._previous_bar: pd.DataFrame = pd.DataFrame()
|
69
|
+
self._previous_datetime = None
|
70
|
+
|
71
|
+
self._long_only = long_only
|
72
|
+
self._short_fee_rate = short_fee_rate
|
73
|
+
self._daily_short_fee_rate = -1.0 + (1.0 + short_fee_rate) ** (1.0 / 252.0)
|
74
|
+
|
75
|
+
self._benchmark = benchmark
|
76
|
+
self._benchmark_size = 0.0
|
77
|
+
|
78
|
+
self._history: Dict[str, List[float]] = {
|
79
|
+
"date": [],
|
80
|
+
"cash": [],
|
81
|
+
"long_position_value": [],
|
82
|
+
"short_position_value": [],
|
83
|
+
"total_value": [],
|
84
|
+
"positions": [],
|
85
|
+
}
|
86
|
+
if benchmark is not None:
|
87
|
+
self._history["benchmark"] = []
|
88
|
+
|
89
|
+
def _update_history(self):
|
90
|
+
"""
|
91
|
+
Updates the history dictionary with the current portfolio state.
|
92
|
+
"""
|
93
|
+
self._history["date"].append(self._current_datetime)
|
94
|
+
self._history["cash"].append(self._cash)
|
95
|
+
self._history["long_position_value"].append(self.long_position_value)
|
96
|
+
self._history["short_position_value"].append(self.short_position_value)
|
97
|
+
self._history["total_value"].append(
|
98
|
+
self.long_position_value + self.short_position_value + self._cash
|
99
|
+
)
|
100
|
+
self._history["positions"].append(len(self._open_positions))
|
101
|
+
if self._benchmark is not None:
|
102
|
+
if len(self._history["benchmark"]) == 0:
|
103
|
+
self.benchmark_size = (
|
104
|
+
self._start_capital
|
105
|
+
/ self._current_bar.loc[self._benchmark, "close"]
|
106
|
+
* (1.0 + self._fees)
|
107
|
+
)
|
108
|
+
self._history["benchmark"].append(
|
109
|
+
self._current_bar.loc[self._benchmark, "close"]
|
110
|
+
* self.benchmark_size
|
111
|
+
* (1.0 - self._fees)
|
112
|
+
)
|
113
|
+
|
114
|
+
def _get_price_for_order(self, order: Order, bar: pd.DataFrame) -> float | None:
|
115
|
+
"""
|
116
|
+
Determines the execution price for a given order based on the current market
|
117
|
+
data.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
order (Order): The order to be executed, containing information about
|
121
|
+
ticker, order type, size, and limit price if applicable.
|
122
|
+
bar (pd.DataFrame): Current market data frame containing OHLC prices for the
|
123
|
+
ticker.
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
float | None: The execution price for the order. None is returned if:
|
127
|
+
- For limit orders: the limit price condition is not met
|
128
|
+
- For open/close orders with limits: the limit price condition is not
|
129
|
+
met
|
130
|
+
|
131
|
+
Raises:
|
132
|
+
ValueError: If the order type is not recognized (not OPEN, CLOSE, or LIMIT)
|
133
|
+
|
134
|
+
Notes:
|
135
|
+
Price determination rules:
|
136
|
+
- For OPEN orders: Uses the opening price
|
137
|
+
- For CLOSE orders: Uses the closing price
|
138
|
+
- For LIMIT orders:
|
139
|
+
- Buy orders: Uses min(open price, limit price) if
|
140
|
+
low price <= limit
|
141
|
+
- Sell orders: Uses max(open price, limit price) if
|
142
|
+
high price >= limit
|
143
|
+
"""
|
144
|
+
ticker = order.ticker
|
145
|
+
if order.order_type == OrderType.OPEN or order.order_type == OrderType.CLOSE:
|
146
|
+
col = "open" if order.order_type == OrderType.OPEN else "close"
|
147
|
+
if order.limit is None:
|
148
|
+
return bar.loc[ticker, col]
|
149
|
+
else:
|
150
|
+
if order.size > 0.0 and bar.loc[ticker, col] <= order.limit:
|
151
|
+
return bar.loc[ticker, col]
|
152
|
+
elif order.size < 0.0 and bar.loc[ticker, col] >= order.limit:
|
153
|
+
return bar.loc[ticker, col]
|
154
|
+
else:
|
155
|
+
return None
|
156
|
+
elif order.order_type == OrderType.LIMIT:
|
157
|
+
if order.size > 0.0 and bar.loc[ticker, "low"] <= order.limit:
|
158
|
+
return min(bar.loc[ticker, "open"], order.limit)
|
159
|
+
elif order.size < 0.0 and bar.loc[ticker, "high"] >= order.limit:
|
160
|
+
return max(bar.loc[ticker, "open"], order.limit)
|
161
|
+
else:
|
162
|
+
return None
|
163
|
+
else:
|
164
|
+
raise ValueError(f"Unknown order type {order.order_type}")
|
165
|
+
|
166
|
+
def _update_closed_positions(
|
167
|
+
self, ticker: str, size: float, price: float, datetime: datetime
|
168
|
+
):
|
169
|
+
"""
|
170
|
+
Updates the list of closed positions for a given trade.
|
171
|
+
|
172
|
+
Updates closed positions tracking when a position is fully or partially closed.
|
173
|
+
For long positions being closed, records the entry price from open position and
|
174
|
+
exit at current price. For short positions being closed, records entry at
|
175
|
+
current price and exit at open position price.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
ticker (str): The ticker symbol of the position
|
179
|
+
size (float): Position size (positive for long, negative for short)
|
180
|
+
price (float): The current closing/reduction price
|
181
|
+
datetime (datetime): Timestamp of the closing/reduction
|
182
|
+
"""
|
183
|
+
if (
|
184
|
+
ticker in self._open_positions
|
185
|
+
and size * self._open_positions[ticker].size < 0.0
|
186
|
+
):
|
187
|
+
# if long position is closed/reduced
|
188
|
+
if self._open_positions[ticker].size > 0.0:
|
189
|
+
self._closed_positions.append(
|
190
|
+
ClosedPosition(
|
191
|
+
self._open_positions[ticker].ticker,
|
192
|
+
min(self._open_positions[ticker].size, abs(size)),
|
193
|
+
self._open_positions[ticker].price,
|
194
|
+
self._open_positions[ticker].datetime,
|
195
|
+
price,
|
196
|
+
datetime,
|
197
|
+
),
|
198
|
+
)
|
199
|
+
# if short position is closed/reduced
|
200
|
+
else:
|
201
|
+
self._closed_positions.append(
|
202
|
+
ClosedPosition(
|
203
|
+
self._open_positions[ticker].ticker,
|
204
|
+
max(self._open_positions[ticker].size, -size),
|
205
|
+
price,
|
206
|
+
datetime,
|
207
|
+
self._open_positions[ticker].price,
|
208
|
+
self._open_positions[ticker].datetime,
|
209
|
+
),
|
210
|
+
)
|
211
|
+
|
212
|
+
def _update_open_positions(
|
213
|
+
self, ticker: str, size: float, price: float, datetime: datetime
|
214
|
+
):
|
215
|
+
"""
|
216
|
+
Updates the open positions for a given ticker.
|
217
|
+
|
218
|
+
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
|
220
|
+
zero, the position is removed. If the ticker does not exist, a new open position
|
221
|
+
is created.
|
222
|
+
|
223
|
+
Args:
|
224
|
+
ticker (str): The ticker symbol of the asset.
|
225
|
+
size (float): The size of the position.
|
226
|
+
price (float): The price at which the position was opened or updated.
|
227
|
+
datetime (datetime): The datetime when the position was opened or updated.
|
228
|
+
"""
|
229
|
+
if ticker in self._open_positions:
|
230
|
+
if size + self._open_positions[ticker].size == 0.0:
|
231
|
+
self._open_positions.pop(ticker)
|
232
|
+
else:
|
233
|
+
open_position_size = self._open_positions[ticker].size + size
|
234
|
+
open_position_price = price
|
235
|
+
open_position_datetime = datetime
|
236
|
+
|
237
|
+
if size * self._open_positions[ticker].size > 0.0:
|
238
|
+
open_position_price = (
|
239
|
+
self._open_positions[ticker].size
|
240
|
+
* self._open_positions[ticker].price
|
241
|
+
+ size * price
|
242
|
+
) / (self._open_positions[ticker].size + size)
|
243
|
+
open_position_datetime = self._open_positions[ticker].datetime
|
244
|
+
elif abs(self._open_positions[ticker].size) > abs(size):
|
245
|
+
open_position_datetime = self._open_positions[ticker].datetime
|
246
|
+
open_position_price = self._open_positions[ticker].price
|
247
|
+
self._open_positions[ticker] = OpenPosition(
|
248
|
+
ticker,
|
249
|
+
open_position_size,
|
250
|
+
open_position_price,
|
251
|
+
open_position_datetime,
|
252
|
+
)
|
253
|
+
else:
|
254
|
+
self._open_positions[ticker] = OpenPosition(
|
255
|
+
ticker,
|
256
|
+
size,
|
257
|
+
price,
|
258
|
+
datetime,
|
259
|
+
)
|
260
|
+
|
261
|
+
def _update_cash(self, order: Order, price: float):
|
262
|
+
"""
|
263
|
+
Updates the cash balance based on the given order and price, accounting for the
|
264
|
+
order size, price, and fees.
|
265
|
+
|
266
|
+
Args:
|
267
|
+
order (Order): The order object containing the size of the order.
|
268
|
+
price (float): The price at which the order is executed.
|
269
|
+
"""
|
270
|
+
if order.size > 0.0:
|
271
|
+
self._cash -= order.size * price * (1.0 + self._fees)
|
272
|
+
else:
|
273
|
+
self._cash -= order.size * price * (1.0 - self._fees)
|
274
|
+
|
275
|
+
def _check_long_only_condition(self, order: Order, datetime: datetime):
|
276
|
+
size = order.size
|
277
|
+
if order.ticker in self._open_positions:
|
278
|
+
size += self._open_positions[order.ticker].size
|
279
|
+
|
280
|
+
if size < 0.0:
|
281
|
+
raise ValueError(
|
282
|
+
f"Short selling is not allowed for {order.ticker} on {datetime}."
|
283
|
+
)
|
284
|
+
|
285
|
+
def _execute_order(
|
286
|
+
self,
|
287
|
+
order: Order,
|
288
|
+
bar: pd.DataFrame,
|
289
|
+
datetime: datetime,
|
290
|
+
) -> bool:
|
291
|
+
"""
|
292
|
+
Executes an order based on the provided bar data and datetime.
|
293
|
+
|
294
|
+
Args:
|
295
|
+
order (Order): The order to be executed.
|
296
|
+
bar (pd.DataFrame): The bar data containing price information.
|
297
|
+
datetime (datetime): The datetime at which the order is executed.
|
298
|
+
|
299
|
+
Returns:
|
300
|
+
bool: True if the order was successfully executed, False otherwise.
|
301
|
+
|
302
|
+
Raises:
|
303
|
+
ValueError: If the long-only condition is violated.
|
304
|
+
"""
|
305
|
+
ticker = order.ticker
|
306
|
+
|
307
|
+
if order.size == 0.0:
|
308
|
+
return False
|
309
|
+
|
310
|
+
if self._long_only:
|
311
|
+
self._check_long_only_condition(order, datetime)
|
312
|
+
|
313
|
+
price = self._get_price_for_order(order, bar)
|
314
|
+
|
315
|
+
# if the order is a limit order and cannot be filled, return
|
316
|
+
if price is None:
|
317
|
+
return False
|
318
|
+
|
319
|
+
# update cash for long and short positions
|
320
|
+
self._update_cash(order, price)
|
321
|
+
|
322
|
+
self._update_closed_positions(ticker, order.size, price, datetime)
|
323
|
+
|
324
|
+
self._update_open_positions(ticker, order.size, price, datetime)
|
325
|
+
|
326
|
+
return True
|
327
|
+
|
328
|
+
def update(
|
329
|
+
self,
|
330
|
+
next_bar: pd.DataFrame,
|
331
|
+
next_datetime: pd.Timestamp,
|
332
|
+
):
|
333
|
+
"""
|
334
|
+
Updates the broker's state with the next trading bar and executes pending
|
335
|
+
orders.
|
336
|
+
|
337
|
+
This method performs several key operations:
|
338
|
+
1. Updates current and previous bar/date references
|
339
|
+
2. Applies short fees for short positions if not in long-only mode
|
340
|
+
3. Sells assets that are no longer in the universe
|
341
|
+
4. Processes pending buy/sell orders
|
342
|
+
5. Updates trading history statistics
|
343
|
+
|
344
|
+
Args:
|
345
|
+
next_bar (pd.DataFrame): The next trading bar data containing at minimum
|
346
|
+
'close' prices for assets
|
347
|
+
next_datetime (pd.Timestamp): The timestamp for the next trading bar
|
348
|
+
|
349
|
+
Notes:
|
350
|
+
- Short fees are calculated using the current bar's closing price
|
351
|
+
- Assets outside the universe are sold at the previous bar's closing price
|
352
|
+
- Orders that cannot be executed due to missing data are skipped with a
|
353
|
+
warning
|
354
|
+
- Good-till-cancel orders that aren't filled are retained for the next bar
|
355
|
+
"""
|
356
|
+
self._previous_bar = self._current_bar
|
357
|
+
self._previous_datetime = self._current_datetime
|
358
|
+
self._current_bar = next_bar
|
359
|
+
self._current_datetime = next_datetime
|
360
|
+
|
361
|
+
# consider short fees
|
362
|
+
if not self._long_only:
|
363
|
+
for ticker in self._open_positions.keys():
|
364
|
+
if self._open_positions[ticker].size < 0.0:
|
365
|
+
price = (
|
366
|
+
self._current_bar.loc[ticker, "close"]
|
367
|
+
if ticker in self._current_bar.index
|
368
|
+
else self._previous_bar.loc[ticker, "close"]
|
369
|
+
)
|
370
|
+
self._cash += (
|
371
|
+
self._open_positions[ticker].size
|
372
|
+
* price
|
373
|
+
* self._daily_short_fee_rate
|
374
|
+
)
|
375
|
+
|
376
|
+
# sell assets out of universe, we use close price of previous bar, since this is
|
377
|
+
# the last price we know
|
378
|
+
ticker_out_of_universe = set()
|
379
|
+
if self._previous_bar.empty:
|
380
|
+
ticker_out_of_universe = set(self._open_positions.keys()) - set(
|
381
|
+
self._current_bar.index
|
382
|
+
)
|
383
|
+
for ticker in ticker_out_of_universe:
|
384
|
+
self._execute_order(
|
385
|
+
Order(ticker, -self._open_positions[ticker].size, OrderType.CLOSE),
|
386
|
+
self._previous_bar,
|
387
|
+
self._previous_datetime,
|
388
|
+
)
|
389
|
+
|
390
|
+
# buy and sell assets
|
391
|
+
remaining_open_orders = []
|
392
|
+
ticker_not_available = set(
|
393
|
+
[open_order.ticker for open_order in self._open_orders]
|
394
|
+
) - set(self._current_bar.index)
|
395
|
+
for open_order in self._open_orders:
|
396
|
+
if open_order.ticker in ticker_not_available:
|
397
|
+
if open_order.size > 0:
|
398
|
+
print(
|
399
|
+
f"{open_order.ticker} could not be bought on {self._current_datetime}." # noqa: E501
|
400
|
+
)
|
401
|
+
else:
|
402
|
+
print(
|
403
|
+
f"{open_order.ticker} could not be sold on {self._current_datetime}." # noqa: E501
|
404
|
+
)
|
405
|
+
continue
|
406
|
+
if (
|
407
|
+
not self._execute_order(
|
408
|
+
open_order, self._current_bar, self._current_datetime
|
409
|
+
)
|
410
|
+
and open_order.good_till_cancel
|
411
|
+
):
|
412
|
+
remaining_open_orders.append(open_order)
|
413
|
+
|
414
|
+
# Retain orders that are good till cancel and were not filled for the next bar
|
415
|
+
self._open_orders = remaining_open_orders
|
416
|
+
|
417
|
+
# update stats
|
418
|
+
self._update_history()
|
419
|
+
|
420
|
+
def liquidate_positions(self):
|
421
|
+
"""
|
422
|
+
Close all open positions in the broker by executing CLOSE orders.
|
423
|
+
|
424
|
+
The method iterates through all open positions and creates close orders with
|
425
|
+
opposite size to the current position size, effectively liquidating all
|
426
|
+
holdings.
|
427
|
+
All orders are executed at the current bar's timestamp.
|
428
|
+
|
429
|
+
No parameters are needed as it operates on the broker's internal state.
|
430
|
+
|
431
|
+
Returns:
|
432
|
+
None
|
433
|
+
"""
|
434
|
+
for ticker in [
|
435
|
+
ticker for ticker in self._open_positions.keys()
|
436
|
+
]: # open_positions is modified during iteration
|
437
|
+
self._execute_order(
|
438
|
+
Order(ticker, -self._open_positions[ticker].size, OrderType.CLOSE),
|
439
|
+
self._current_bar,
|
440
|
+
self._current_datetime,
|
441
|
+
)
|
442
|
+
|
443
|
+
def place_order(self, order: Order):
|
444
|
+
"""
|
445
|
+
Places a new order in the broker's open orders list.
|
446
|
+
|
447
|
+
Args:
|
448
|
+
order (Order): The order object to be placed in the open orders.
|
449
|
+
|
450
|
+
Note:
|
451
|
+
The order is appended to the internal _open_orders list and remains there
|
452
|
+
until it is either executed or cancelled.
|
453
|
+
"""
|
454
|
+
self._open_orders.append(order)
|
455
|
+
|
456
|
+
@property
|
457
|
+
def short_position_value(self) -> float:
|
458
|
+
"""
|
459
|
+
Gets the total market value of all short positions including fees.
|
460
|
+
|
461
|
+
Calculates the sum of (price * quantity) for all positions with negative size,
|
462
|
+
using the most recent closing price available. Transaction fees are included
|
463
|
+
in the final value.
|
464
|
+
|
465
|
+
Returns:
|
466
|
+
float: Total value of short positions (negative number), including fees.
|
467
|
+
"""
|
468
|
+
value = 0.0
|
469
|
+
for ticker in self._open_positions.keys():
|
470
|
+
if self._open_positions[ticker].size < 0.0:
|
471
|
+
price = (
|
472
|
+
self._current_bar.loc[ticker, "close"]
|
473
|
+
if ticker in self._current_bar.index
|
474
|
+
else self._previous_bar.loc[ticker, "close"]
|
475
|
+
)
|
476
|
+
value += price * self._open_positions[ticker].size * (1.0 + self._fees)
|
477
|
+
return value
|
478
|
+
|
479
|
+
@property
|
480
|
+
def long_position_value(self) -> float:
|
481
|
+
"""
|
482
|
+
Gets the total market value of all long positions after fees.
|
483
|
+
|
484
|
+
Calculates the sum of (price * quantity) for all positions with positive size,
|
485
|
+
using the most recent closing price available. Transaction fees are deducted
|
486
|
+
from the final value to reflect net liquidation value.
|
487
|
+
|
488
|
+
Returns:
|
489
|
+
float: Total value of long positions (positive number), net of fees.
|
490
|
+
"""
|
491
|
+
value = 0.0
|
492
|
+
for ticker in self._open_positions.keys():
|
493
|
+
if self._open_positions[ticker].size > 0.0:
|
494
|
+
price = (
|
495
|
+
self._current_bar.loc[ticker, "close"]
|
496
|
+
if ticker in self._current_bar.index
|
497
|
+
else self._previous_bar.loc[ticker, "close"]
|
498
|
+
)
|
499
|
+
value += price * self._open_positions[ticker].size * (1.0 - self._fees)
|
500
|
+
return value
|
501
|
+
|
502
|
+
@property
|
503
|
+
def portfolio_value(self) -> float:
|
504
|
+
"""
|
505
|
+
Gets the total portfolio value including cash and all positions.
|
506
|
+
|
507
|
+
Calculates net portfolio value by summing:
|
508
|
+
- Available cash balance
|
509
|
+
- Long positions market value (less fees)
|
510
|
+
- Short positions market value (including fees)
|
511
|
+
|
512
|
+
Returns:
|
513
|
+
float: Total portfolio value in base currency.
|
514
|
+
"""
|
515
|
+
return self._cash + self.long_position_value + self.short_position_value
|
516
|
+
|
517
|
+
@property
|
518
|
+
def open_positions(self) -> Dict[str, OpenPosition]:
|
519
|
+
"""Gets a dictionary of currently active trading positions.
|
520
|
+
|
521
|
+
Maps ticker symbols to OpenPosition objects containing:
|
522
|
+
- ticker: Financial instrument identifier
|
523
|
+
- size: Position size (positive=long, negative=short)
|
524
|
+
- price: Average entry price
|
525
|
+
- datetime: Position opening timestamp
|
526
|
+
|
527
|
+
Returns:
|
528
|
+
Dict[str, OpenPosition]: Dictionary mapping ticker symbols to positions.
|
529
|
+
Returns a defensive copy to prevent external modifications.
|
530
|
+
"""
|
531
|
+
return self._open_positions.copy()
|
532
|
+
|
533
|
+
@property
|
534
|
+
def closed_positions(self) -> List[ClosedPosition]:
|
535
|
+
"""
|
536
|
+
Gets a list of all completed trades.
|
537
|
+
|
538
|
+
Each ClosedPosition contains the complete trade lifecycle:
|
539
|
+
- Entry price and timestamp
|
540
|
+
- Exit price and timestamp
|
541
|
+
- Position size and direction
|
542
|
+
- Ticker symbol
|
543
|
+
|
544
|
+
Returns a defensive copy to prevent external modifications.
|
545
|
+
|
546
|
+
Returns:
|
547
|
+
List[ClosedPosition]: Chronological list of completed trades.
|
548
|
+
"""
|
549
|
+
return self._closed_positions.copy()
|
550
|
+
|
551
|
+
@property
|
552
|
+
def history(self) -> Dict[str, List[float]]:
|
553
|
+
"""Gets the historical performance metrics dictionary.
|
554
|
+
|
555
|
+
Contains time series data tracking portfolio metrics:
|
556
|
+
- 'cash': Available cash balance
|
557
|
+
- 'portfolio': Total portfolio value
|
558
|
+
- 'short_value': Value of short positions
|
559
|
+
- 'long_value': Value of long positions
|
560
|
+
- 'benchmark': Benchmark performance if specified
|
561
|
+
|
562
|
+
Returns:
|
563
|
+
Dict[str, List[float]]: Dictionary mapping metric names to value histories.
|
564
|
+
Returns a defensive copy to prevent external modifications.
|
565
|
+
"""
|
566
|
+
return self._history.copy()
|
567
|
+
|
568
|
+
@property
|
569
|
+
def cash(self) -> float:
|
570
|
+
"""Gets the current available cash balance.
|
571
|
+
|
572
|
+
Represents uninvested capital that can be used for new positions. Updated after
|
573
|
+
each trade to reflect fees and position changes.
|
574
|
+
|
575
|
+
Returns:
|
576
|
+
float: Available cash balance in base currency.
|
577
|
+
"""
|
578
|
+
return self._cash
|
579
|
+
|
580
|
+
@property
|
581
|
+
def benchmark(self) -> str:
|
582
|
+
"""Gets the benchmark symbol used for performance comparison.
|
583
|
+
|
584
|
+
The benchmark tracks a reference asset (e.g., market index) to evaluate relative
|
585
|
+
strategy performance. Returns None if no benchmark was specified.
|
586
|
+
|
587
|
+
Returns:
|
588
|
+
str: Ticker symbol of the benchmark instrument.
|
589
|
+
"""
|
590
|
+
return self._benchmark
|
kissbt/engine.py
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
import pandas as pd
|
2
|
+
|
3
|
+
from kissbt.broker import Broker
|
4
|
+
from kissbt.strategy import Strategy
|
5
|
+
|
6
|
+
|
7
|
+
class Engine:
|
8
|
+
"""Coordinates execution of a trading strategy using broker actions.
|
9
|
+
|
10
|
+
This class drives the main loop that processes market data, updates the
|
11
|
+
broker's state, and calls the strategy logic for each segment of data.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
broker (Broker): The Broker instance for managing trades and positions.
|
15
|
+
strategy (Strategy): The trading strategy to be applied to the data.
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, broker: Broker, strategy: Strategy) -> None:
|
19
|
+
self.broker = broker
|
20
|
+
self.strategy = strategy
|
21
|
+
|
22
|
+
def run(self, data: pd.DataFrame) -> None:
|
23
|
+
for current_date, current_data in data.groupby("date"):
|
24
|
+
current_data.index = current_data.index.droplevel("date")
|
25
|
+
|
26
|
+
self.broker.update(current_data, current_date)
|
27
|
+
self.strategy(current_data, current_date)
|
28
|
+
|
29
|
+
self.broker.liquidate_positions()
|
kissbt/entities.py
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from enum import Enum
|
3
|
+
|
4
|
+
import pandas as pd
|
5
|
+
|
6
|
+
|
7
|
+
class OrderType(Enum):
|
8
|
+
OPEN = "open"
|
9
|
+
CLOSE = "close"
|
10
|
+
LIMIT = "limit"
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass(frozen=True)
|
14
|
+
class Order:
|
15
|
+
"""
|
16
|
+
Trading order representation. Immutable to ensure data integrity and thread safety.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
ticker (str): Ticker symbol of the traded asset
|
20
|
+
size (float): Order size (positive for buy, negative for sell)
|
21
|
+
order_type (OrderType, optional): Order type, defaults to OPEN
|
22
|
+
limit (float | None, optional): Limit price if applicable, defaults to None
|
23
|
+
good_till_cancel (bool, optional): Order validity flag, defaults to False
|
24
|
+
"""
|
25
|
+
|
26
|
+
ticker: str
|
27
|
+
size: float
|
28
|
+
order_type: OrderType = OrderType.OPEN
|
29
|
+
limit: float | None = None
|
30
|
+
good_till_cancel: bool = False
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass(frozen=True)
|
34
|
+
class OpenPosition:
|
35
|
+
"""
|
36
|
+
Immutable representation of an open trading position.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
ticker (str): Financial instrument identifier
|
40
|
+
size (float): Position size (positive for long, negative for short)
|
41
|
+
price (float): Opening price of the position
|
42
|
+
datetime (datetime): Position opening timestamp
|
43
|
+
"""
|
44
|
+
|
45
|
+
ticker: str
|
46
|
+
size: float
|
47
|
+
price: float
|
48
|
+
datetime: pd.Timestamp
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass(frozen=True)
|
52
|
+
class ClosedPosition:
|
53
|
+
"""
|
54
|
+
Immutable representation of a completed trading transaction.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
ticker (str): Financial instrument identifier
|
58
|
+
size (float): Position size (positive for long, negative for short)
|
59
|
+
purchase_price (float): Entry price of the position
|
60
|
+
purchase_datetime (pd.Timestamp): Position entry timestamp
|
61
|
+
selling_price (float): Exit price of the position
|
62
|
+
selling_datetime (pd.Timestamp): Position exit timestamp
|
63
|
+
"""
|
64
|
+
|
65
|
+
ticker: str
|
66
|
+
size: float
|
67
|
+
purchase_price: float
|
68
|
+
purchase_datetime: pd.Timestamp
|
69
|
+
selling_price: float
|
70
|
+
selling_datetime: pd.Timestamp
|