-
Notifications
You must be signed in to change notification settings - Fork 204
Expand file tree
/
Copy pathcontext.py
More file actions
1791 lines (1490 loc) · 62.1 KB
/
Copy pathcontext.py
File metadata and controls
1791 lines (1490 loc) · 62.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import logging
from datetime import datetime, timezone
from typing import List
from investing_algorithm_framework.services import ConfigurationService, \
MarketCredentialService, OrderService, PortfolioConfigurationService, \
PortfolioService, PositionService, TradeService, DataProviderService, \
TradeStopLossService, TradeTakeProfitService
from investing_algorithm_framework.domain import OrderStatus, OrderType, \
OrderSide, OperationalException, Portfolio, RoundingService, \
BACKTESTING_FLAG, INDEX_DATETIME, Order, \
Position, Trade, TradeStatus, MarketCredential, TradeStopLoss, \
TradeTakeProfit
logger = logging.getLogger("investing_algorithm_framework")
class Context:
"""
Context class to store the state of the algorithm and
give access to objects such as orders, positions, trades and
portfolio.
"""
def __init__(
self,
configuration_service: ConfigurationService,
portfolio_configuration_service: PortfolioConfigurationService,
portfolio_service: PortfolioService,
position_service: PositionService,
order_service: OrderService,
market_credential_service: MarketCredentialService,
trade_service: TradeService,
trade_stop_loss_service: TradeStopLossService,
trade_take_profit_service: TradeTakeProfitService,
data_provider_service: DataProviderService
):
self.configuration_service: ConfigurationService = \
configuration_service
self.portfolio_configuration_service: PortfolioConfigurationService = \
portfolio_configuration_service
self.portfolio_service: PortfolioService = portfolio_service
self.position_service: PositionService = position_service
self.order_service: OrderService = order_service
self.market_credential_service: MarketCredentialService = \
market_credential_service
self.data_provider_service: DataProviderService = data_provider_service
self.trade_service: TradeService = trade_service
self.trade_stop_loss_service: TradeStopLossService = \
trade_stop_loss_service
self.trade_take_profit_service: TradeTakeProfitService = \
trade_take_profit_service
def _validate_target_symbol(self, target_symbol, market=None):
"""
Validate the target_symbol for order creation:
1. Prevents orders where target_symbol equals trading_symbol
(e.g. EUR/EUR).
2. Checks that a data source exists for the
target_symbol/trading_symbol combination (e.g. BTC/EUR).
Args:
target_symbol: The symbol of the asset to trade
market: The market to check against
Raises:
OperationalException: If validation fails
"""
portfolio = self.portfolio_service.find({"market": market})
trading_symbol = portfolio.trading_symbol
# Check target_symbol != trading_symbol
if target_symbol.upper() == trading_symbol.upper():
raise OperationalException(
f"target_symbol '{target_symbol}' is the same as "
f"the trading_symbol '{trading_symbol}'. "
f"This would result in a "
f"'{trading_symbol}/{trading_symbol}' "
f"order which is not valid. "
f"To skip this check, set validate_symbol=False "
f"or omit the parameter."
)
# Check that a data source is registered for this pair
expected_symbol = f"{target_symbol}/{trading_symbol}".upper()
known_symbols = set()
if self.data_provider_service.data_provider_index is not None:
for data_source, _ in \
self.data_provider_service \
.data_provider_index.get_all():
if data_source.symbol is not None:
known_symbols.add(data_source.symbol.upper())
if expected_symbol not in known_symbols:
sorted_symbols = sorted(known_symbols)
raise OperationalException(
f"No data source registered for '{expected_symbol}'. "
f"A data source is required to track price history. "
f"Registered data source symbols: {sorted_symbols}. "
f"To skip this check, set validate_symbol=False "
f"or omit the parameter."
)
@property
def config(self):
"""
Function to get a config instance. This allows users when
having access to the algorithm instance also to read the
configs of the app.
"""
return self.configuration_service.get_config()
def get_config(self):
"""
Function to get a config instance. This allows users when
having access to the algorithm instance also to read the
configs of the app.
"""
return self.configuration_service.get_config()
def create_order(
self,
target_symbol,
price,
order_type,
order_side,
amount,
market=None,
execute=True,
validate=True,
sync=True,
validate_symbol=False
) -> Order:
"""
Function to create an order. This function will create an order
and execute it if the execute parameter is set to True. If the
validate parameter is set to True, the order will be validated
Args:
target_symbol: The symbol of the asset to trade
price: The price of the asset
order_type: The type of the order
order_side: The side of the order
amount: The amount of the asset to trade
market: The market to trade the asset
execute: If set to True, the order will be executed
validate: If set to True, the order will be validated
sync: If set to True, the created order will be synced
with the portfolio of the algorithm.
validate_symbol: Default False. If set to True,
validates that target_symbol is not the trading_symbol.
Returns:
The order created
"""
if validate_symbol:
self._validate_target_symbol(target_symbol, market=market)
portfolio = self.portfolio_service.find({"market": market})
order_data = {
"target_symbol": target_symbol,
"price": price,
"amount": amount,
"order_type": order_type,
"order_side": order_side,
"portfolio_id": portfolio.id,
"status": OrderStatus.CREATED.value,
"trading_symbol": portfolio.trading_symbol,
}
if BACKTESTING_FLAG in self.configuration_service.config \
and self.configuration_service.config[BACKTESTING_FLAG]:
order_data["created_at"] = \
self.configuration_service.config[INDEX_DATETIME]
return self.order_service.create(
order_data, execute=execute, validate=validate, sync=sync
)
def has_balance(self, symbol, amount, market=None):
"""
Function to check if the portfolio has enough balance to
create an order. This function will return True if the
portfolio has enough balance to create an order, False
otherwise.
Parameters:
symbol: The symbol of the asset
amount: The amount of the asset
market: The market of the asset
Returns:
Boolean: True if the portfolio has enough balance
"""
portfolio = self.portfolio_service.find({"market": market})
position = self.position_service.find(
{"portfolio": portfolio.id, "symbol": symbol}
)
if position is None:
return False
return position.get_amount() >= amount
def create_limit_order(
self,
target_symbol,
price,
order_side,
amount=None,
amount_trading_symbol=None,
percentage=None,
percentage_of_portfolio=None,
percentage_of_position=None,
precision=None,
market=None,
execute=True,
validate=True,
sync=True,
metadata=None,
validate_symbol=False
) -> Order:
"""
Function to create a limit order. This function will create a limit
order and execute it if the execute parameter is set to True. If the
validate parameter is set to True, the order will be validated
Args:
target_symbol: The symbol of the asset to trade
price: The price of the asset
order_side: The side of the order
amount (optional): The amount of the asset to trade
amount_trading_symbol (optional): The amount of the
trading symbol to trade
percentage (optional): The percentage of the portfolio
to allocate to the
order
percentage_of_portfolio (optional): The percentage
of the portfolio to allocate to the order
percentage_of_position (optional): The percentage
of the position to allocate to
the order. (Only supported for SELL orders)
precision (optional): The precision of the amount
market (optional): The market to trade the asset
execute (optional): Default True. If set to True,
the order will be executed
validate (optional): Default True. If set to
True, the order will be validated
sync (optional): Default True. If set to True,
the created order will be synced with the
portfolio of the algorithm
validate_symbol (optional): Default False. If set to True,
validates that target_symbol is not the trading_symbol.
Returns:
Order: Instance of the order created
"""
if validate_symbol:
self._validate_target_symbol(target_symbol, market=market)
portfolio = self.portfolio_service.find({"market": market})
if percentage_of_portfolio is not None:
if not OrderSide.BUY.equals(order_side):
raise OperationalException(
"Percentage of portfolio is only supported for BUY orders."
)
net_size = portfolio.get_net_size()
size = net_size * (percentage_of_portfolio / 100)
amount = size / price
elif percentage_of_position is not None:
if not OrderSide.SELL.equals(order_side):
raise OperationalException(
"Percentage of position is only supported for SELL orders."
)
position = self.position_service.find(
{
"symbol": target_symbol,
"portfolio": portfolio.id
}
)
amount = position.get_amount() * (percentage_of_position / 100)
elif percentage is not None:
net_size = portfolio.get_net_size()
size = net_size * (percentage / 100)
amount = size / price
if precision is not None:
amount = RoundingService.round_down(amount, precision)
if amount_trading_symbol is not None:
amount = amount_trading_symbol / price
if amount is None:
raise OperationalException(
"The amount parameter is required to create a limit order." +
"Either the amount, amount_trading_symbol, percentage, " +
"percentage_of_portfolio or percentage_of_position "
"parameter must be specified."
)
logger.info(
f"Creating limit order: {target_symbol} "
f"{order_side} {amount} @ {price}"
)
order_data = {
"target_symbol": target_symbol,
"price": price,
"amount": amount,
"order_type": OrderType.LIMIT.value,
"order_side": OrderSide.from_value(order_side).value,
"portfolio_id": portfolio.id,
"status": OrderStatus.CREATED.value,
"trading_symbol": portfolio.trading_symbol,
}
if metadata is not None:
order_data["metadata"] = metadata
if BACKTESTING_FLAG in self.configuration_service.config \
and self.configuration_service.config[BACKTESTING_FLAG]:
order_data["created_at"] = \
self.configuration_service.config[INDEX_DATETIME]
return self.order_service.create(
order_data, execute=execute, validate=validate, sync=sync
)
def create_limit_sell_order(
self,
target_symbol,
price,
amount=None,
percentage_of_position=None,
market=None,
portfolio_id=None
) -> Order:
"""
Function to create a limit sell order. This function will create
a limit sell order. If the amount parameter is specified, the
order will be created with the specified amount. If the
percentage_of_position parameter is specified, the order will be
created with the percentage of the position specified. If neither
the amount nor the percentage_of_position parameter is specified,
an OperationalException will be raised.
Args:
target_symbol (str): The symbol of the asset to sell
price (float): The price at which to sell the asset
amount (float, optional): The amount of the asset to sell
percentage_of_position (float, optional): The percentage of the
position to sell.
market (str, optional): the portfolio corresponding to the market
to sell the asset
portfolio_id: (str, optional): The ID of the portfolio to sell
the asset from.
Returns:
Order: The order created
"""
if amount is None and percentage_of_position is None:
raise OperationalException(
"Either amount or percentage_of_position must be specified "
"to create a limit sell order."
)
if portfolio_id is not None:
portfolio = self.portfolio_service.get(portfolio_id)
elif market is not None:
portfolio = self.portfolio_service.find({"market": market})
else:
portfolio = self.portfolio_service.get_all()[0]
if percentage_of_position is not None:
position = self.position_service.find(
{
"symbol": target_symbol,
"portfolio": portfolio.id
}
)
amount = position.get_amount() * (percentage_of_position / 100)
logger.info(
f"Creating limit order: {target_symbol} "
f"SELL {amount} @ {price}"
)
order_data = {
"target_symbol": target_symbol,
"price": price,
"amount": amount,
"order_type": OrderType.LIMIT.value,
"order_side": OrderSide.SELL.value,
"portfolio_id": portfolio.id,
"status": OrderStatus.CREATED.value,
"trading_symbol": portfolio.trading_symbol,
}
if BACKTESTING_FLAG in self.configuration_service.config \
and self.configuration_service.config[BACKTESTING_FLAG]:
order_data["created_at"] = \
self.configuration_service.config[INDEX_DATETIME]
return self.order_service.create(order_data)
def create_limit_buy_order(
self,
target_symbol,
price,
amount=None,
percentage_of_portfolio=None,
market=None,
portfolio_id=None
) -> Order:
"""
Function to create a limit buy order. This function will create
a limit buy order. If the amount parameter is specified, the
order will be created with the specified amount. If the
percentage_of_portfolio parameter is specified, the order will be
created with the percentage of the portfolio specified. If neither
the amount nor the percentage_of_portfolio parameter is specified,
an OperationalException will be raised.
Args:
target_symbol (str): The symbol of the asset to buy
price (float): The price at which to buy the asset
amount (float, optional): The amount of the asset to buy
percentage_of_portfolio (float, optional): The percentage of the
portfolio to buy.
market (str, optional): the portfolio corresponding to the market
to buy the asset
portfolio_id (str, optional): The ID of the portfolio to buy
the asset from.
Returns:
Order: The order created
"""
if amount is None and percentage_of_portfolio is None:
raise OperationalException(
"Either amount or percentage_of_portfolio must be specified "
"to create a limit buy order."
)
if portfolio_id is not None:
portfolio = self.portfolio_service.get(portfolio_id)
elif market is not None:
portfolio = self.portfolio_service.find({"market": market})
else:
portfolio = self.portfolio_service.get_all()[0]
if percentage_of_portfolio is not None:
net_size = portfolio.get_net_size()
size = net_size * (percentage_of_portfolio / 100)
amount = size / price
logger.info(
f"Creating limit order: {target_symbol} "
f"BUY {amount} @ {price}"
)
order_data = {
"target_symbol": target_symbol,
"price": price,
"amount": amount,
"order_type": OrderType.LIMIT.value,
"order_side": OrderSide.BUY.value,
"portfolio_id": portfolio.id,
"status": OrderStatus.CREATED.value,
"trading_symbol": portfolio.trading_symbol,
}
if BACKTESTING_FLAG in self.configuration_service.config \
and self.configuration_service.config[BACKTESTING_FLAG]:
order_data["created_at"] = \
self.configuration_service.config[INDEX_DATETIME]
return self.order_service.create(
order_data, execute=True, validate=True, sync=True
)
def get_portfolio(self, market=None) -> Portfolio:
"""
Function to get the portfolio of the algorithm. This function
will return the portfolio of the algorithm. If the market
parameter is specified, the portfolio of the specified market
will be returned.
Parameters:
market: The market of the portfolio
Returns:
Portfolio: The portfolio of the algorithm
"""
if market is None:
portfolio = self.portfolio_service.get_all()[0]
else:
portfolio = self.portfolio_service.find({"market": market})
# Retrieve positions
positions = self.position_service.get_all(
{"portfolio": portfolio.id}
)
if BACKTESTING_FLAG in self.configuration_service.config \
and self.configuration_service.config[BACKTESTING_FLAG]:
date = self.configuration_service.config[INDEX_DATETIME]
else:
date = datetime.now(tz=timezone.utc)
allocated = 0.0
for position in positions:
if position.symbol != portfolio.trading_symbol:
ticker = self.data_provider_service.get_ticker_data(
symbol=f"{position.symbol}/{portfolio.trading_symbol}",
market=portfolio.market,
date=date
)
if ticker is not None and "bid" in ticker:
allocated += position.get_amount() * ticker["bid"]
portfolio.allocated = allocated
return portfolio
def get_latest_price(self, symbol, market=None):
if BACKTESTING_FLAG in self.configuration_service.config \
and self.configuration_service.config[BACKTESTING_FLAG]:
date = self.configuration_service.config[INDEX_DATETIME]
else:
date = datetime.now(tz=timezone.utc)
ticker = self.data_provider_service.get_ticker_data(
symbol=symbol,
market=market,
date=date
)
return ticker['bid'] if ticker and 'bid' in ticker else None
def get_portfolios(self):
"""
Function to get all portfolios of the algorithm. This function
will return all portfolios of the algorithm.
Returns:
List[Portfolio]: A list of all portfolios of the algorithm
"""
return self.portfolio_service.get_all()
def get_unallocated(self, market=None) -> float:
"""
Function to get the unallocated balance of the portfolio. This
function will return the unallocated balance of the portfolio.
If the market parameter is specified, the unallocated balance
of the specified market will be returned.
Args:
market: The market of the portfolio
Returns:
float: The unallocated balance of the portfolio
"""
if market:
portfolio = self.portfolio_service.find({{"market": market}})
else:
portfolio = self.portfolio_service.get_all()[0]
trading_symbol = portfolio.trading_symbol
return self.position_service.find(
{"portfolio": portfolio.id, "symbol": trading_symbol}
).get_amount()
def get_total_size(self):
"""
Returns the total size of the portfolio.
The total size of the portfolio is the unallocated balance and the
allocated balance of the portfolio.
Returns:
float: The total size of the portfolio
"""
return self.get_unallocated() + self.get_allocated()
def get_order(
self,
reference_id=None,
market=None,
target_symbol=None,
trading_symbol=None,
order_side=None,
order_type=None
) -> Order:
"""
Function to retrieve an order.
Exception is thrown when no param has been provided.
Args:
reference_id [optional] (int): id given by the external
market or exchange.
market [optional] (str): the market that the order was
executed on.
target_symbol [optional] (str): the symbol of the asset
that the order was executed
"""
query_params = {}
if reference_id:
query_params["reference_id"] = reference_id
if target_symbol:
query_params["target_symbol"] = target_symbol
if trading_symbol:
query_params["trading_symbol"] = trading_symbol
if order_side:
query_params["order_side"] = order_side
if order_type:
query_params["order_type"] = order_type
if market:
portfolio = self.portfolio_service.find({"market": market})
positions = self.position_service.get_all(
{"portfolio": portfolio.id}
)
query_params["position"] = [position.id for position in positions]
if not query_params:
raise OperationalException(
"No parameters provided to get order."
)
return self.order_service.find(query_params)
def get_orders(
self,
target_symbol=None,
status=None,
order_type=None,
order_side=None,
market=None
) -> List[Order]:
if market is None:
portfolio = self.portfolio_service.get_all()[0]
else:
portfolio = self.portfolio_service.find({"market": market})
positions = self.position_service.get_all({"portfolio": portfolio.id})
return self.order_service.get_all(
{
"position": [position.id for position in positions],
"target_symbol": target_symbol,
"status": status,
"order_type": order_type,
"order_side": order_side
}
)
def get_positions(
self,
market=None,
identifier=None,
amount_gt=None,
amount_gte=None,
amount_lt=None,
amount_lte=None
) -> List[Position]:
"""
Function to get all positions. This function will return all
positions that match the specified query parameters. If the
market parameter is specified, the positions of the specified
market will be returned. If the identifier parameter is
specified, the positions of the specified portfolio will be
returned. If the amount_gt parameter is specified, the positions
with an amount greater than the specified amount will be returned.
If the amount_gte parameter is specified, the positions with an
amount greater than or equal to the specified amount will be
returned. If the amount_lt parameter is specified, the positions
with an amount less than the specified amount will be returned.
If the amount_lte parameter is specified, the positions with an
amount less than or equal to the specified amount will be returned.
Parameters:
market: The market of the portfolio where the positions are
identifier: The identifier of the portfolio
amount_gt: The amount of the asset must be greater than this
amount_gte: The amount of the asset must be greater than or
equal to this
amount_lt: The amount of the asset must be less than this
amount_lte: The amount of the asset must be less than or equal
to this
Returns:
List[Position]: A list of positions that match the query parameters
"""
query_params = {}
if market is not None:
query_params["market"] = market
if identifier is not None:
query_params["identifier"] = identifier
if amount_gt is not None:
query_params["amount_gt"] = amount_gt
if amount_gte is not None:
query_params["amount_gte"] = amount_gte
if amount_lt is not None:
query_params["amount_lt"] = amount_lt
if amount_lte is not None:
query_params["amount_lte"] = amount_lte
portfolios = self.portfolio_service.get_all(query_params)
if not portfolios:
raise OperationalException("No portfolio found.")
portfolio = portfolios[0]
return self.position_service.get_all(
{"portfolio": portfolio.id}
)
def get_position(self, symbol, market=None, identifier=None) -> Position:
"""
Function to get a position. This function will return the
position that matches the specified query parameters. If the
market parameter is specified, the position of the specified
market will be returned. If the identifier parameter is
specified, the position of the specified portfolio will be
returned.
Parameters:
symbol: The symbol of the asset that represents the position
market: The market of the portfolio where the position is located
identifier: The identifier of the portfolio
Returns:
Position: The position that matches the query parameters
"""
query_params = {}
if market is not None:
query_params["market"] = market
if identifier is not None:
query_params["identifier"] = identifier
portfolios = self.portfolio_service.get_all(query_params)
if not portfolios:
raise OperationalException("No portfolio found.")
portfolio = portfolios[0]
try:
return self.position_service.find(
{"portfolio": portfolio.id, "symbol": symbol}
)
except OperationalException:
return None
def has_position(
self,
symbol,
market=None,
identifier=None,
amount_gt=0,
amount_gte=None,
amount_lt=None,
amount_lte=None
):
"""
Function to check if a position exists. This function will return
True if a position exists, False otherwise. This function will check
if the amount > 0 condition by default.
Parameters:
param symbol: The symbol of the asset
param market: The market of the asset
param identifier: The identifier of the portfolio
param amount_gt: The amount of the asset must be greater than this
param amount_gte: The amount of the asset must be greater than
or equal to this
param amount_lt: The amount of the asset must be less than this
param amount_lte: The amount of the asset must be less than
or equal to this
Returns:
Boolean: True if a position exists, False otherwise
"""
return self.position_exists(
symbol=symbol,
market=market,
identifier=identifier,
amount_gt=amount_gt,
amount_gte=amount_gte,
amount_lt=amount_lt,
amount_lte=amount_lte
)
def position_exists(
self,
symbol,
market=None,
identifier=None,
amount_gt=None,
amount_gte=None,
amount_lt=None,
amount_lte=None
) -> bool:
"""
Function to check if a position exists. This function will return
True if a position exists, False otherwise. This function will
not check the amount > 0 condition by default. If you want to
check if a position exists with an amount greater than 0, you
can use the amount_gt parameter. If you want to check if a
position exists with an amount greater than or equal to a
certain amount, you can use the amount_gte parameter. If you
want to check if a position exists with an amount less than a
certain amount, you can use the amount_lt parameter. If you want
to check if a position exists with an amount less than or equal
to a certain amount, you can use the amount_lte parameter.
It is not recommended to use this method directly because it can
have adverse effects on the algorithm. It is recommended to use
the has_position method instead.
param symbol: The symbol of the asset
param market: The market of the asset
param identifier: The identifier of the portfolio
param amount_gt: The amount of the asset must be greater than this
param amount_gte: The amount of the asset must be greater than
or equal to this
param amount_lt: The amount of the asset must be less than this
param amount_lte: The amount of the asset must be less than
or equal to this
return: True if a position exists, False otherwise
"""
query_params = {}
if market is not None:
query_params["market"] = market
if identifier is not None:
query_params["identifier"] = identifier
if amount_gt is not None:
query_params["amount_gt"] = amount_gt
if amount_gte is not None:
query_params["amount_gte"] = amount_gte
if amount_lt is not None:
query_params["amount_lt"] = amount_lt
if amount_lte is not None:
query_params["amount_lte"] = amount_lte
query_params["symbol"] = symbol
return self.position_service.exists(query_params)
def get_position_percentage_of_portfolio_by_net_size(
self, symbol, market=None, identifier=None
) -> float:
"""
Returns the percentage of the portfolio that is allocated to a
position. This is calculated by dividing the cost of the position
by the total net size of the portfolio.
The total net size of the portfolio is the initial balance of the
portfolio plus the all the net gains of your trades.
"""
query_params = {}
if market is not None:
query_params["market"] = market
if identifier is not None:
query_params["identifier"] = identifier
portfolios = self.portfolio_service.get_all(query_params)
if not portfolios:
raise OperationalException("No portfolio found.")
portfolio = portfolios[0]
position = self.position_service.find(
{"portfolio": portfolio.id, "symbol": symbol}
)
net_size = portfolio.get_net_size()
return (position.cost / net_size) * 100
def close_position(
self,
position=None,
symbol=None,
portfolio=None,
precision=None,
price=None
) -> Order:
"""
Function to close a position. This function will close a position
by creating a market order to sell the position. If the precision
parameter is specified, the amount of the order will be rounded
down to the specified precision.
Args:
position (Optional): The position to close
symbol (Optional): The symbol of the asset
portfolio (Optional): The portfolio where the position is located
precision (Optional): The precision of the amount
price (Optional[Float]): The price with which the position needs
to be closed.
Returns:
Order: The order created to close the position
"""
query_params = {}
if position is None and (symbol is None and portfolio is None):
raise OperationalException(
"Either position or symbol and portfolio parameters must "
"be specified to close a position."
)
if position is not None:
query_params["id"] = position.id
query_params["symbol"] = position.symbol
if symbol is not None:
query_params["symbol"] = symbol
if portfolio is not None:
query_params["portfolio"] = portfolio.id
position = self.position_service.find(query_params)
portfolio = self.portfolio_service.get(position.portfolio_id)
if position.get_amount() == 0:
logger.warning("Cannot close position. Amount is 0.")
return None
if position.get_symbol() == portfolio.get_trading_symbol():
raise OperationalException(
"Cannot close position. The position is the same as the "
"trading symbol of the portfolio."
)
for order in self.order_service \
.get_all(
{
"position": position.id,
"status": OrderStatus.OPEN.value
}
):
self.order_service.cancel_order(order)
target_symbol = position.get_symbol()
symbol = f"{target_symbol.upper()}/{portfolio.trading_symbol.upper()}"
if price is None:
ticker = self.data_provider_service.get_ticker_data(
symbol=symbol,
market=portfolio.market,
date=self.config[INDEX_DATETIME]
)
price = ticker["bid"]
logger.info(
f"Closing position {position.symbol} "
f"with amount {position.get_amount()} "
f"at price {price}"
)
return self.create_limit_order(
target_symbol=position.symbol,
amount=position.get_amount(),
order_side=OrderSide.SELL.value,
price=price,
precision=precision,
)
def get_allocated(self, market=None, identifier=None) -> float: