dijkies 0.0.3__py3-none-any.whl → 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.
- dijkies/data_pipeline.py +33 -2
- dijkies/deployment.py +173 -60
- dijkies/exceptions.py +23 -0
- dijkies/exchange_market_api.py +5 -9
- dijkies/executors.py +82 -150
- dijkies/performance.py +3 -5
- dijkies/strategy.py +67 -6
- {dijkies-0.0.3.dist-info → dijkies-0.1.1.dist-info}/METADATA +167 -31
- dijkies-0.1.1.dist-info/RECORD +12 -0
- {dijkies-0.0.3.dist-info → dijkies-0.1.1.dist-info}/WHEEL +1 -1
- dijkies/backtest.py +0 -98
- dijkies/credentials.py +0 -6
- dijkies/evaluate.py +0 -235
- dijkies-0.0.3.dist-info/RECORD +0 -15
dijkies/executors.py
CHANGED
|
@@ -1,38 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
1
4
|
from abc import ABC, abstractmethod
|
|
5
|
+
from decimal import ROUND_DOWN, Decimal, getcontext
|
|
2
6
|
from typing import Literal, Optional, Union
|
|
3
7
|
|
|
4
|
-
from pydantic import BaseModel
|
|
5
|
-
|
|
6
|
-
import uuid
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
8
|
import pandas as pd
|
|
10
9
|
from pandas.core.series import Series
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import logging
|
|
15
|
-
|
|
16
|
-
from decimal import Decimal, getcontext, ROUND_DOWN
|
|
17
|
-
from tenacity import retry, wait_fixed, stop_after_attempt
|
|
18
|
-
|
|
10
|
+
from pydantic import BaseModel
|
|
19
11
|
from python_bitvavo_api.bitvavo import Bitvavo
|
|
20
12
|
|
|
21
13
|
from dijkies.exceptions import (
|
|
22
|
-
NoOrderFoundError,
|
|
23
|
-
MultipleOrdersFoundError,
|
|
24
|
-
PlaceOrderError,
|
|
25
14
|
GetOrderInfoError,
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
InsufficientBalanceError,
|
|
16
|
+
InsufficientOrderValueError,
|
|
17
|
+
MultipleOrdersFoundError,
|
|
18
|
+
NoOrderFoundError,
|
|
28
19
|
)
|
|
29
20
|
|
|
30
|
-
|
|
21
|
+
SUPPORTED_EXCHANGES = Literal["bitvavo", "backtest"]
|
|
31
22
|
|
|
32
23
|
|
|
33
24
|
class Order(BaseModel):
|
|
34
25
|
order_id: str
|
|
35
|
-
exchange:
|
|
26
|
+
exchange: SUPPORTED_EXCHANGES
|
|
36
27
|
market: str
|
|
37
28
|
time_created: int
|
|
38
29
|
time_canceled: Union[int, None] = None
|
|
@@ -173,12 +164,6 @@ class ExchangeAssetClient(ABC):
|
|
|
173
164
|
def __init__(self, state: State) -> None:
|
|
174
165
|
self.state = state
|
|
175
166
|
|
|
176
|
-
def clear_credentials(self) -> None:
|
|
177
|
-
raise MethodNotDefinedError()
|
|
178
|
-
|
|
179
|
-
def set_credentials(self, credentials: Credentials) -> None:
|
|
180
|
-
raise MethodNotDefinedError()
|
|
181
|
-
|
|
182
167
|
@abstractmethod
|
|
183
168
|
def place_limit_buy_order(
|
|
184
169
|
self, base: str, limit_price: float, amount_in_quote: float
|
|
@@ -221,7 +206,7 @@ class BacktestExchangeAssetClient(ExchangeAssetClient):
|
|
|
221
206
|
super().__init__(state)
|
|
222
207
|
self.fee_market_order = fee_market_order
|
|
223
208
|
self.fee_limit_order = fee_limit_order
|
|
224
|
-
self.current_candle = pd.Series({"high":
|
|
209
|
+
self.current_candle = pd.Series({"high": 80000, "low": 78000, "close": 79000})
|
|
225
210
|
|
|
226
211
|
def update_current_candle(self, current_candle: Series) -> None:
|
|
227
212
|
self.current_candle = current_candle
|
|
@@ -336,6 +321,7 @@ class BacktestExchangeAssetClient(ExchangeAssetClient):
|
|
|
336
321
|
filled = order.on_hold
|
|
337
322
|
filled_quote = order.on_hold * order.limit_price # type: ignore
|
|
338
323
|
fee = filled_quote * fee_limit_order
|
|
324
|
+
order.fee = fee
|
|
339
325
|
return Order(
|
|
340
326
|
order_id=order.order_id,
|
|
341
327
|
exchange=order.exchange,
|
|
@@ -393,7 +379,7 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
393
379
|
bitvavo_api_key: str,
|
|
394
380
|
bitvavo_api_secret_key: str,
|
|
395
381
|
operator_id: int,
|
|
396
|
-
logger: logging.Logger
|
|
382
|
+
logger: logging.Logger,
|
|
397
383
|
) -> None:
|
|
398
384
|
super().__init__(state)
|
|
399
385
|
self.operator_id = operator_id
|
|
@@ -409,19 +395,9 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
409
395
|
)
|
|
410
396
|
self.logger = logger
|
|
411
397
|
|
|
412
|
-
def set_credentials(self, api_key: str, api_secret_key: str) -> None:
|
|
413
|
-
self.bitvavo.APIKEY = api_key
|
|
414
|
-
self.bitvavo.APISECRET = api_secret_key
|
|
415
|
-
|
|
416
|
-
def clear_credentials(self) -> None:
|
|
417
|
-
self.bitvavo.APIKEY = ""
|
|
418
|
-
self.bitvavo.APISECRET = ""
|
|
419
|
-
|
|
420
398
|
def quantity_decimals(self, base: str) -> int:
|
|
421
399
|
trading_pair = base + "-EUR"
|
|
422
|
-
return self.bitvavo.markets(
|
|
423
|
-
{'market': trading_pair}
|
|
424
|
-
)['quantityDecimals']
|
|
400
|
+
return self.bitvavo.markets({"market": trading_pair})["quantityDecimals"]
|
|
425
401
|
|
|
426
402
|
@staticmethod
|
|
427
403
|
def __closest_valid_price(price: float) -> float:
|
|
@@ -445,7 +421,14 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
445
421
|
|
|
446
422
|
return float(corrected)
|
|
447
423
|
|
|
448
|
-
|
|
424
|
+
def get_balance(self, base: str) -> dict[str, float]:
|
|
425
|
+
balance = self.bitvavo.balance({"symbol": base})
|
|
426
|
+
if balance:
|
|
427
|
+
balance = balance[0]
|
|
428
|
+
else:
|
|
429
|
+
balance = {"available": 0, "inOrder": 0}
|
|
430
|
+
return balance
|
|
431
|
+
|
|
449
432
|
def place_limit_buy_order(
|
|
450
433
|
self, base: str, limit_price: float, amount_in_quote: float
|
|
451
434
|
) -> Order:
|
|
@@ -468,30 +451,23 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
468
451
|
},
|
|
469
452
|
)
|
|
470
453
|
if "errorCode" in response:
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
454
|
+
error_code = response["errorCode"]
|
|
455
|
+
if error_code in [107, 108, 109]:
|
|
456
|
+
time.sleep(3)
|
|
457
|
+
order = self.place_limit_buy_order(base, limit_price, amount_in_quote)
|
|
458
|
+
return order
|
|
459
|
+
elif error_code == 216:
|
|
460
|
+
balance = self.get_balance(base)
|
|
461
|
+
raise InsufficientBalanceError(balance, amount_in_base)
|
|
462
|
+
elif error_code == 217:
|
|
463
|
+
raise InsufficientOrderValueError()
|
|
464
|
+
else:
|
|
465
|
+
raise Exception(str(response))
|
|
481
466
|
|
|
482
|
-
error: \n\n
|
|
483
|
-
|
|
484
|
-
{response}
|
|
485
|
-
"""
|
|
486
|
-
self.logger.error(error_message)
|
|
487
|
-
raise PlaceOrderError(error_message)
|
|
488
|
-
|
|
489
|
-
self.logger.info(f"limit order placed: {json.dumps(response)}")
|
|
490
467
|
order = order_from_bitvavo_response(response)
|
|
491
468
|
self.state.add_order(order)
|
|
492
469
|
return order
|
|
493
470
|
|
|
494
|
-
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
495
471
|
def place_limit_sell_order(
|
|
496
472
|
self, base: str, limit_price: float, amount_in_base: float
|
|
497
473
|
) -> Order:
|
|
@@ -499,7 +475,7 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
499
475
|
|
|
500
476
|
quantity_decimals = self.quantity_decimals(base)
|
|
501
477
|
|
|
502
|
-
factor = 1 / (10
|
|
478
|
+
factor = 1 / (10**quantity_decimals)
|
|
503
479
|
amount_in_base = round(
|
|
504
480
|
(float(amount_in_base) // factor) * factor,
|
|
505
481
|
quantity_decimals,
|
|
@@ -518,33 +494,24 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
518
494
|
},
|
|
519
495
|
)
|
|
520
496
|
if "errorCode" in response:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
497
|
+
error_code = response["errorCode"]
|
|
498
|
+
if error_code in [107, 108, 109]:
|
|
499
|
+
time.sleep(3)
|
|
500
|
+
order = self.place_limit_sell_order(base, limit_price, amount_in_base)
|
|
501
|
+
return order
|
|
502
|
+
elif error_code == 216:
|
|
503
|
+
balance = self.get_balance(base)
|
|
504
|
+
raise InsufficientBalanceError(balance, amount_in_base)
|
|
505
|
+
elif error_code == 217:
|
|
506
|
+
raise InsufficientOrderValueError()
|
|
507
|
+
else:
|
|
508
|
+
raise Exception(str(response))
|
|
531
509
|
|
|
532
|
-
error: \n\n
|
|
533
|
-
|
|
534
|
-
{response}
|
|
535
|
-
"""
|
|
536
|
-
self.logger.error(error_message)
|
|
537
|
-
raise PlaceOrderError(error_message)
|
|
538
|
-
|
|
539
|
-
self.logger.info(f"limit order placed: {json.dumps(response)}")
|
|
540
510
|
order = order_from_bitvavo_response(response)
|
|
541
511
|
self.state.add_order(order)
|
|
542
512
|
return order
|
|
543
513
|
|
|
544
|
-
|
|
545
|
-
def place_market_buy_order(
|
|
546
|
-
self, base: str, amount_in_quote: float
|
|
547
|
-
) -> Order:
|
|
514
|
+
def place_market_buy_order(self, base: str, amount_in_quote: float) -> Order:
|
|
548
515
|
trading_pair = base + "-EUR"
|
|
549
516
|
|
|
550
517
|
amount_in_quote = str(round(float(amount_in_quote), 2))
|
|
@@ -556,34 +523,30 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
556
523
|
body={"amountQuote": amount_in_quote, "operatorId": self.operator_id},
|
|
557
524
|
)
|
|
558
525
|
if "errorCode" in response:
|
|
526
|
+
error_code = response["errorCode"]
|
|
527
|
+
if error_code in [107, 108, 109]:
|
|
528
|
+
time.sleep(3)
|
|
529
|
+
order = self.place_market_buy_order(base, amount_in_quote)
|
|
530
|
+
return order
|
|
531
|
+
elif error_code == 216:
|
|
532
|
+
balance = self.get_balance("EUR")
|
|
533
|
+
raise InsufficientBalanceError(balance, amount_in_quote)
|
|
534
|
+
elif error_code == 217:
|
|
535
|
+
raise InsufficientOrderValueError()
|
|
536
|
+
else:
|
|
537
|
+
raise Exception(str(response))
|
|
559
538
|
|
|
560
|
-
error_message = \
|
|
561
|
-
f"""
|
|
562
|
-
failed to place market buy-order\n\n
|
|
563
|
-
trading_pair: {trading_pair}\n
|
|
564
|
-
amount_in_quote: {amount_in_quote}\n\n
|
|
565
|
-
error:\n\n
|
|
566
|
-
{response}
|
|
567
|
-
"""
|
|
568
|
-
|
|
569
|
-
self.logger.error(error_message)
|
|
570
|
-
raise PlaceOrderError(error_message)
|
|
571
|
-
|
|
572
|
-
self.logger.info(f"market buy order placed: {json.dumps(response)}")
|
|
573
539
|
order = order_from_bitvavo_response(response)
|
|
574
540
|
time.sleep(3)
|
|
575
541
|
order = self.get_order_info(order)
|
|
576
542
|
self.state.process_filled_order(order)
|
|
577
543
|
return order
|
|
578
544
|
|
|
579
|
-
|
|
580
|
-
def place_market_sell_order(
|
|
581
|
-
self, base: str, amount_in_base: float
|
|
582
|
-
) -> Order:
|
|
545
|
+
def place_market_sell_order(self, base: str, amount_in_base: float) -> Order:
|
|
583
546
|
trading_pair = base + "-EUR"
|
|
584
547
|
quantity_decimals = self.quantity_decimals(base)
|
|
585
548
|
|
|
586
|
-
factor = 1 / (10
|
|
549
|
+
factor = 1 / (10**quantity_decimals)
|
|
587
550
|
amount_in_base = round(
|
|
588
551
|
(float(amount_in_base) // factor) * factor,
|
|
589
552
|
quantity_decimals,
|
|
@@ -595,47 +558,33 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
595
558
|
body={"amount": str(amount_in_base), "operatorId": self.operator_id},
|
|
596
559
|
)
|
|
597
560
|
if "errorCode" in response:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
561
|
+
error_code = response["errorCode"]
|
|
562
|
+
if error_code in [107, 108, 109]:
|
|
563
|
+
time.sleep(3)
|
|
564
|
+
order = self.place_market_sell_order(base, amount_in_base)
|
|
565
|
+
return order
|
|
566
|
+
elif error_code == 216:
|
|
567
|
+
balance = self.get_balance(base)
|
|
568
|
+
raise InsufficientBalanceError(balance, amount_in_base)
|
|
569
|
+
elif error_code == 217:
|
|
570
|
+
raise InsufficientOrderValueError()
|
|
571
|
+
else:
|
|
572
|
+
raise Exception(str(response))
|
|
573
|
+
|
|
610
574
|
order = order_from_bitvavo_response(response)
|
|
611
575
|
time.sleep(3)
|
|
612
576
|
order = self.get_order_info(order)
|
|
613
577
|
self.state.process_filled_order(order)
|
|
614
578
|
return order
|
|
615
579
|
|
|
616
|
-
|
|
617
|
-
def get_order_info(
|
|
618
|
-
self, order: Order
|
|
619
|
-
) -> Order:
|
|
580
|
+
def get_order_info(self, order: Order) -> Order:
|
|
620
581
|
|
|
621
|
-
response = self.bitvavo.getOrder(
|
|
622
|
-
market=order.market, orderId=order.order_id
|
|
623
|
-
)
|
|
582
|
+
response = self.bitvavo.getOrder(market=order.market, orderId=order.order_id)
|
|
624
583
|
if "errorCode" in response:
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
failed to retrieve order information\n\n
|
|
628
|
-
order: {order.order_id}\n
|
|
629
|
-
trading_pair: {order.market}\n\n
|
|
630
|
-
error: \n\n
|
|
631
|
-
|
|
632
|
-
{response}
|
|
633
|
-
"""
|
|
634
|
-
self.logger.exception(error_message)
|
|
635
|
-
raise GetOrderInfoError(response)
|
|
584
|
+
if response["errorCode"] == 240:
|
|
585
|
+
raise GetOrderInfoError(response)
|
|
636
586
|
return order_from_bitvavo_response(response)
|
|
637
587
|
|
|
638
|
-
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
639
588
|
def cancel_order(self, order: Order) -> Order:
|
|
640
589
|
response = self.bitvavo.cancelOrder(
|
|
641
590
|
market=order.market,
|
|
@@ -643,23 +592,6 @@ class BitvavoExchangeAssetClient(ExchangeAssetClient):
|
|
|
643
592
|
operatorId=self.operator_id,
|
|
644
593
|
)
|
|
645
594
|
if "errorCode" in response:
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
failed to cancel order\n\n
|
|
649
|
-
trading_pair: {order.market}\n
|
|
650
|
-
order_id: {order.order_id}\n\n
|
|
651
|
-
error:\n\n
|
|
652
|
-
|
|
653
|
-
{response}
|
|
654
|
-
"""
|
|
655
|
-
|
|
656
|
-
self.logger.exception(error_message)
|
|
657
|
-
|
|
658
|
-
if not (
|
|
659
|
-
response["errorCode"] == 240 and
|
|
660
|
-
"No active order found." in response["error"]
|
|
661
|
-
):
|
|
662
|
-
raise CancelOrderError(response)
|
|
663
|
-
|
|
664
|
-
self.logger.info(f"order cancelled: {order.order_id}")
|
|
595
|
+
if response["errorCode"] != 240:
|
|
596
|
+
raise GetOrderInfoError(response)
|
|
665
597
|
self.state.cancel_order(order)
|
dijkies/performance.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from pandas.core.series import Series as PandasSeries
|
|
5
|
-
|
|
6
1
|
import uuid
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
7
3
|
from datetime import datetime
|
|
8
4
|
|
|
5
|
+
import numpy as np
|
|
9
6
|
from pandas.core.series import Series
|
|
7
|
+
from pandas.core.series import Series as PandasSeries
|
|
10
8
|
from pydantic import BaseModel
|
|
11
9
|
|
|
12
10
|
from dijkies.executors import State
|
dijkies/strategy.py
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
+
from datetime import datetime, timedelta
|
|
3
4
|
|
|
5
|
+
import pandas as pd
|
|
4
6
|
from pandas.core.frame import DataFrame as PandasDataFrame
|
|
5
7
|
|
|
6
|
-
from dijkies.executors import ExchangeAssetClient
|
|
7
8
|
from dijkies.data_pipeline import DataPipeline
|
|
9
|
+
from dijkies.exceptions import (
|
|
10
|
+
DataTimeWindowShorterThanSuggestedAnalysisWindowError,
|
|
11
|
+
InvalidExchangeAssetClientError,
|
|
12
|
+
InvalidTypeForTimeColumnError,
|
|
13
|
+
MissingOHLCVColumnsError,
|
|
14
|
+
TimeColumnNotDefinedError,
|
|
15
|
+
)
|
|
16
|
+
from dijkies.executors import BacktestExchangeAssetClient, ExchangeAssetClient
|
|
17
|
+
from dijkies.performance import PerformanceInformationRow
|
|
8
18
|
|
|
9
19
|
|
|
10
20
|
class Strategy(ABC):
|
|
@@ -56,13 +66,64 @@ class Strategy(ABC):
|
|
|
56
66
|
def analysis_dataframe_size_in_minutes(self) -> int:
|
|
57
67
|
pass
|
|
58
68
|
|
|
59
|
-
@property
|
|
60
|
-
@abstractmethod
|
|
61
|
-
def exchange(self) -> str:
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
69
|
def get_data_pipeline(self) -> DataPipeline:
|
|
65
70
|
"""
|
|
66
71
|
implement this method for deployement
|
|
67
72
|
"""
|
|
68
73
|
raise NotImplementedError()
|
|
74
|
+
|
|
75
|
+
def backtest(self, data: PandasDataFrame) -> PandasDataFrame:
|
|
76
|
+
"""
|
|
77
|
+
This method runs the backtest. It expects data, this should have the following properties:
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# validate args
|
|
81
|
+
|
|
82
|
+
if "time" not in data.columns:
|
|
83
|
+
raise TimeColumnNotDefinedError()
|
|
84
|
+
|
|
85
|
+
if not pd.api.types.is_datetime64_any_dtype(data.time):
|
|
86
|
+
raise InvalidTypeForTimeColumnError()
|
|
87
|
+
|
|
88
|
+
lookback_in_min = self.analysis_dataframe_size_in_minutes
|
|
89
|
+
timespan_data_in_min = (data.time.max() - data.time.min()).total_seconds() / 60
|
|
90
|
+
|
|
91
|
+
if lookback_in_min > timespan_data_in_min:
|
|
92
|
+
raise DataTimeWindowShorterThanSuggestedAnalysisWindowError()
|
|
93
|
+
|
|
94
|
+
if not {"open", "high", "low", "close", "volume"}.issubset(data.columns):
|
|
95
|
+
raise MissingOHLCVColumnsError()
|
|
96
|
+
|
|
97
|
+
if not isinstance(self.executor, BacktestExchangeAssetClient):
|
|
98
|
+
raise InvalidExchangeAssetClientError()
|
|
99
|
+
|
|
100
|
+
start_time = data.iloc[0].time + timedelta(minutes=lookback_in_min)
|
|
101
|
+
simulation_df: PandasDataFrame = data.loc[data.time >= start_time]
|
|
102
|
+
start_candle = simulation_df.iloc[0]
|
|
103
|
+
start_value_in_quote = self.state.total_value_in_quote(start_candle.open)
|
|
104
|
+
result: list[PerformanceInformationRow] = []
|
|
105
|
+
|
|
106
|
+
def get_analysis_df(
|
|
107
|
+
data: PandasDataFrame, current_time: datetime, look_back_in_min: int
|
|
108
|
+
) -> PandasDataFrame:
|
|
109
|
+
start_analysis_df = current_time - timedelta(minutes=look_back_in_min)
|
|
110
|
+
|
|
111
|
+
analysis_df = data.loc[
|
|
112
|
+
(data.time >= start_analysis_df) & (data.time <= current_time)
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
return analysis_df
|
|
116
|
+
|
|
117
|
+
for _, candle in simulation_df.iterrows():
|
|
118
|
+
analysis_df = get_analysis_df(data, candle.time, lookback_in_min)
|
|
119
|
+
self.executor.update_current_candle(candle)
|
|
120
|
+
|
|
121
|
+
self.run(analysis_df)
|
|
122
|
+
|
|
123
|
+
result.append(
|
|
124
|
+
PerformanceInformationRow.from_objects(
|
|
125
|
+
candle, start_candle, self.state, start_value_in_quote
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return pd.DataFrame([r.model_dump() for r in result])
|