kissbt 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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