-
-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathasynchronous.py
More file actions
1523 lines (1267 loc) · 61.3 KB
/
Copy pathasynchronous.py
File metadata and controls
1523 lines (1267 loc) · 61.3 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 asyncio
import json
import re
import sys
from datetime import timedelta
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from ..config import Config
from ..validator import Validator
if TYPE_CHECKING:
from ..BinaryOptionsToolsV2 import Logger, RawPocketOption
if sys.version_info < (3, 10):
async def anext(iterator):
"""Polyfill for anext for Python < 3.10"""
return await iterator.__anext__()
class AsyncSubscription:
def __init__(self, subscription):
"""Asynchronous Iterator over json objects"""
self.subscription = subscription
def __aiter__(self):
return self
async def __anext__(self):
return json.loads(await anext(self.subscription))
class RawHandler:
"""
Handler for advanced raw WebSocket message operations.
Provides low-level access to send messages and receive filtered responses
based on a validator. Each handler maintains its own message stream.
"""
def __init__(self, rust_handler):
"""
Initialize RawHandler with a Rust handler instance.
Args:
rust_handler: The underlying RawHandlerRust instance from PyO3
"""
self._handler = rust_handler
async def send_text(self, message: str) -> None:
"""
Send a text message through this handler.
Args:
message: Text message to send
Example:
```python
await handler.send_text('42["ping"]')
```
"""
await self._handler.send_text(message)
async def send_binary(self, data: bytes) -> None:
"""
Send a binary message through this handler.
Args:
data: Binary data to send
Example:
```python
await handler.send_binary(b'\\x00\\x01\\x02')
```
"""
await self._handler.send_binary(data)
async def send_and_wait(self, message: str) -> str:
"""
Send a message and wait for the next matching response.
Args:
message: Message to send
Returns:
str: The first response that matches this handler's validator
Example:
```python
response = await handler.send_and_wait('42["getBalance"]')
data = json.loads(response)
```
"""
return await self._handler.send_and_wait(message)
async def wait_next(self) -> str:
"""
Wait for the next message that matches this handler's validator.
Returns:
str: The next matching message
Example:
```python
message = await handler.wait_next()
print(f"Received: {message}")
```
"""
return await self._handler.wait_next()
async def subscribe(self):
"""
Subscribe to messages matching this handler's validator.
Returns:
AsyncIterator[str]: Stream of matching messages
Example:
```python
stream = await handler.subscribe()
async for message in stream:
data = json.loads(message)
print(f"Update: {data}")
```
"""
return self._handler.subscribe()
def id(self) -> str:
"""
Get the unique ID of this handler.
Returns:
str: Handler UUID
"""
return self._handler.id()
async def close(self) -> None:
"""
Close this handler and clean up resources.
Note: The handler is automatically cleaned up when it goes out of scope.
This method is a no-op; resource cleanup is handled by the Rust Drop implementation.
"""
self._handler = None # Release reference to allow Rust Drop
def sanitize_and_validate_ssid(ssid: str, logger: "Logger") -> str:
"""Sanitize SSID format and validate session payload semantics.
Performs three layers of validation:
1. Format normalization (fix shell-stripped quotes)
2. JSON structure validation (parseable payload)
3. Semantic validation (required fields, session format)
Args:
ssid: Raw SSID string from user input
logger: Logger instance for warnings
Returns:
Sanitized SSID string ready for the Rust backend
Raises:
ValueError: If the SSID payload is missing required fields
"""
ssid = re.sub(r"""42\[['"]?auth['"]?\s*,""", '42["auth",', ssid, count=1)
if not ssid.startswith("42["):
logger.warn(f"SSID does not start with '42[': {ssid[:20]}...")
return ssid
try:
payload = json.loads(ssid[2:])
except json.JSONDecodeError:
logger.warn("SSID payload is not valid JSON after sanitization")
return ssid
if not isinstance(payload, list) or len(payload) < 2:
logger.warn("SSID payload is not a valid Socket.IO auth array")
return ssid
auth_data = payload[1] if len(payload) > 1 else {}
if not isinstance(auth_data, dict):
logger.warn("SSID auth data is not a dictionary")
return ssid
warnings_list = []
required_fields = ["session", "uid"]
for field in required_fields:
if field not in auth_data:
warnings_list.append(f"missing required field '{field}'")
session = auth_data.get("session", "")
if session and not re.match(r"^[a-zA-Z0-9_\-]{10,}$", str(session)):
warnings_list.append(f"session token has unexpected format (length={len(str(session))})")
uid = auth_data.get("uid")
if uid is not None:
try:
uid_int = int(uid)
if uid_int <= 0:
warnings_list.append(f"uid should be a positive integer, got {uid_int}")
except (ValueError, TypeError):
warnings_list.append(f"uid is not a valid integer: {uid!r}")
platform = auth_data.get("platform")
if platform is not None and platform not in (1, 2):
warnings_list.append(f"unexpected platform value: {platform}")
is_demo = auth_data.get("isDemo")
if is_demo is not None and is_demo not in (0, 1):
warnings_list.append(f"isDemo should be 0 or 1, got {is_demo}")
for w in warnings_list:
logger.warn(f"SSID validation: {w}")
critical = [w for w in warnings_list if "missing required field" in w]
if critical:
raise ValueError(
"Invalid SSID: " + "; ".join(critical) + ". "
"The SSID payload must contain 'session' and 'uid' fields. "
"Ensure your SSID follows the format: 42['auth',{{'session':'...','uid':123,...}}]"
)
return ssid
# This file contains all the async code for the PocketOption Module
class PocketOptionAsync:
def __init__(self, ssid: str, url: Optional[str] = None, config: Optional[Union[Config, dict, str]] = None, **_):
"""
Initializes a new PocketOptionAsync instance.
This class provides an asynchronous interface for interacting with the Pocket Option trading platform.
It supports custom WebSocket URLs and configuration options for fine-tuning the connection behavior.
Args:
ssid (str): Session ID for authentication with Pocket Option platform
url (str | None, optional): Custom WebSocket server URL. Defaults to None, using platform's default URL.
config (Config | dict | str, optional): Configuration options. Can be provided as:
- Config object: Direct instance of Config class
- dict: Dictionary of configuration parameters
- str: JSON string containing configuration parameters
Configuration parameters include:
- max_allowed_loops (int): Maximum number of event loop iterations
- sleep_interval (int): Sleep time between operations in milliseconds
- reconnect_time (int): Time to wait before reconnection attempts in seconds
- connection_initialization_timeout_secs (int): Connection initialization timeout
- timeout_secs (int): General operation timeout
- urls (List[str]): List of fallback WebSocket URLs
**_: Additional keyword arguments (ignored)
Examples:
Basic usage:
```python
client = PocketOptionAsync("your-session-id")
```
With custom WebSocket URL:
```python
client = PocketOptionAsync("your-session-id", url="wss://custom-server.com/ws")
```
Warning: This class is designed for asynchronous operations and should be used within an async context.
Note:
- The configuration becomes locked once initialized and cannot be modified afterwards
- Custom URLs provided in the `url` parameter take precedence over URLs in the configuration
- Invalid configuration values will raise appropriate exceptions
"""
try:
from ..BinaryOptionsToolsV2 import RawPocketOption
except ImportError:
from BinaryOptionsToolsV2 import RawPocketOption
from ..tracing import Logger, LogBuilder
self.logger = Logger()
self._ssid_valid = True
if ssid is not None:
ssid = sanitize_and_validate_ssid(ssid, self.logger)
if not ssid.startswith("42["):
self._ssid_valid = False
else:
try:
payload = json.loads(ssid[2:])
if not isinstance(payload, list) or len(payload) < 2:
self._ssid_valid = False
except json.JSONDecodeError:
self._ssid_valid = False
else:
self.logger.warn("SSID is None, connection will likely fail")
self._ssid_valid = False
if config is not None:
if isinstance(config, dict):
self.config = Config.from_dict(config)
elif isinstance(config, str):
self.config = Config.from_json(config)
elif isinstance(config, Config):
self.config = config
else:
raise ValueError("Config type mismatch")
if url is not None:
self.config.urls.insert(0, url)
else:
self.config = Config()
if url is not None:
self.config.urls.insert(0, url)
if self.config.terminal_logging:
try:
lb = LogBuilder()
lb.terminal(level=self.config.log_level)
lb.build()
except Exception:
pass
self.client: "RawPocketOption" = RawPocketOption.new_with_config(ssid, self.config.pyconfig)
async def __aenter__(self):
"""
Context manager entry. Waits for assets to be loaded.
"""
await self.wait_for_assets()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""
Context manager exit. Shuts down the client and its runner.
"""
await self.shutdown()
async def _place_trade(self, method, asset: str, amount: float, time: int, check_win: bool) -> Tuple[str, Dict]:
"""Internal helper to place a trade and optionally wait for the result."""
trade_id, trade = await method(asset, amount, time)
if check_win:
return trade_id, await self.check_win(trade_id, timeout_seconds=time + 30)
trade = json.loads(trade)
return trade_id, trade
async def buy(self, asset: str, amount: float, time: int, check_win: bool = False) -> Tuple[str, Dict]:
"""Places a buy (call) order."""
return await self._place_trade(self.client.buy, asset, amount, time, check_win)
async def sell(self, asset: str, amount: float, time: int, check_win: bool = False) -> Tuple[str, Dict]:
"""Places a sell (put) order."""
return await self._place_trade(self.client.sell, asset, amount, time, check_win)
async def check_win(self, id: str, timeout_seconds: Optional[int] = None) -> dict:
"""
Checks the result of a specific trade.
Args:
id (str): ID of the trade to check.
timeout_seconds (Optional[int]): Maximum time in seconds to wait for the trade result.
If None, uses the configured default (default: 300s).
When called from buy()/sell() with check_win=True, this is automatically
set to trade_duration + 15 seconds to account for server processing.
Returns:
dict: Trade result containing:
- result: "win", "loss", or "draw"
- profit: Profit/loss amount
- details: Additional trade details
- timestamp: Result timestamp
Raises:
ValueError: If trade_id is invalid
TimeoutError: If result check times out
Example:
```python
# For a 60-second trade, use a 75-second timeout
result = await client.check_win(trade_id, timeout_seconds=75)
```
"""
# Set a reasonable timeout to prevent hanging
# Default to 300 seconds to accommodate longer trade durations (e.g., 300s timeframes)
if timeout_seconds is None:
timeout_seconds = getattr(self.config, "check_win_timeout_secs", 300)
# If timeout_seconds is 0, we wait indefinitely
actual_timeout = timeout_seconds if timeout_seconds > 0 else None
try:
# Use asyncio.wait_for as additional protection against hanging
trade = await asyncio.wait_for(self._get_trade_result(id), timeout=actual_timeout)
return trade
except asyncio.TimeoutError:
raise TimeoutError(f"Timeout waiting for trade result for ID: {id}")
async def get_deal_end_time(self, trade_id: str) -> Optional[int]:
"""
Returns the expected close time of a deal as a Unix timestamp.
Returns None if the deal is not found.
"""
return await self.client.get_deal_end_time(trade_id)
async def _get_trade_result(self, id: str) -> dict:
"""Internal method to retrieve and classify trade result with timeout protection.
Fetches the trade result from the Rust backend, parses the JSON response,
and classifies the outcome as 'win', 'loss', or 'draw' based on the profit value.
Args:
id (str): The unique trade identifier to look up.
Returns:
dict: Trade result dictionary containing:
- id (str): The trade identifier
- profit (float): The profit/loss amount
- result (str): Classified outcome ("win", "loss", or "draw")
- Additional fields from the server response
Raises:
Exception: Wraps any error from the Rust client with context about the trade ID.
ValueError: If the profit field cannot be converted to float.
KeyError: If the response dict is missing required fields.
json.JSONDecodeError: If the server response is not valid JSON.
"""
try:
trade = await self.client.check_win(id)
trade = json.loads(trade)
win = float(trade["profit"])
except (json.JSONDecodeError, KeyError, ValueError, TypeError) as e:
raise ValueError(f"Invalid trade result response for ID {id}: {e}") from e
except Exception as e:
raise RuntimeError(f"Error getting trade result for ID {id}: {e}") from e
if win > 0:
trade["result"] = "win"
elif win == 0:
trade["result"] = "draw"
else:
trade["result"] = "loss"
return trade
async def candles(self, asset: str, period: int) -> List[Dict]:
"""
Retrieves historical candle data for an asset.
Args:
asset (str): Trading asset (e.g., "EURUSD_otc")
period (int): Candle timeframe in seconds (e.g., 60 for 1-minute candles)
Returns:
List[Dict]: List of candles, each containing:
- time: Candle timestamp
- open: Opening price
- high: Highest price
- low: Lowest price
- close: Closing price
"""
candles = await self.client.candles(asset, period)
return json.loads(candles)
async def get_candles(self, asset: str, period: int, offset: int) -> List[Dict]:
"""
Retrieves historical candle data for an asset.
Args:
asset (str): Trading asset (e.g., "EURUSD_otc")
period (int): Historical period in seconds to fetch
offset (int): Candle timeframe in seconds (e.g., 60 for 1-minute candles)
Returns:
List[Dict]: List of candles, each containing:
- time: Candle timestamp
- open: Opening price
- high: Highest price
- low: Lowest price
- close: Closing price
Note:
Available timeframes: 1, 5, 15, 30, 60, 300 seconds
Maximum period depends on the timeframe
"""
candles = await self.client.get_candles(asset, period, offset)
return json.loads(candles)
async def get_candles_advanced(self, asset: str, period: int, offset: int, time: int) -> List[Dict]:
"""
Retrieves historical candle data for an asset.
Args:
asset (str): Trading asset (e.g., "EURUSD_otc")
period (int): Historical period in seconds to fetch
offset (int): Candle timeframe in seconds (e.g., 60 for 1-minute candles)
time (int): Time to fetch candles from
Returns:
List[Dict]: List of candles, each containing:
- time: Candle timestamp
- open: Opening price
- high: Highest price
- low: Lowest price
- close: Closing price
Note:
Available timeframes: 1, 5, 15, 30, 60, 300 seconds
Maximum period depends on the timeframe
"""
candles = await self.client.get_candles_advanced(asset, period, offset, time)
return json.loads(candles)
async def balance(self) -> float:
"""
Retrieves current account balance.
Returns:
float: Account balance in account currency
Note:
Updates in real-time as trades are completed
"""
return await self.client.balance()
async def opened_deals(self) -> List[str]:
"""Retrieves a list of all currently open (active) deals.
This method returns all deals ids that are currently active/open on the account,
including both pending and executed trades that have not yet closed.
Returns:
List[str]: List of currently opened deals IDs in UUID format.
Raises:
ConnectionError: If the client is not connected to the platform
ValueError: If the response format is invalid
Examples:
Basic usage:
```python
async with PocketOptionAsync(ssid) as client:
open_deals_ids = await client.opened_deals()
open_deals = [await client.get_opened_deal(deal_id) for deal_id in open_deals_ids]
for deal in open_deals:
print(f"Deal {deal['id']}: {deal['asset']} {deal['direction']}")
```
Filtering active deals:
```python
async def monitor_open_deals(client):
deals_ids = await client.opened_deals()
deals = [await client.get_opened_deal(deal_id) for deal_id in deals_ids]
total_value = sum(d['amount'] for d in deals)
print(f"Open deals: {len(deals)}, Total exposure: {total_value}")
```
"""
return json.loads(await self.client.opened_deals())
async def get_opened_deal(self, id: str) -> Optional[Dict]:
"""
Retrieves details of a specific opened deal by its ID.
Args:
id (str): The unique identifier of the deal to retrieve
Returns:
Optional[Dict]: A dictionary containing deal details if found, otherwise None.
Deal details include:
- id: Unique deal identifier
- asset: Trading asset symbol
- amount: Trade amount
- direction: "buy" or "sell"
- entry_price: Entry price of the trade
- expiry: Expiration timestamp
- timestamp: Deal creation timestamp
Raises:
ConnectionError: If the client is not connected to the platform
ValueError: If the response format is invalid
Examples:
Fetch specific deal details:
```python
async with PocketOptionAsync(ssid) as client:
deal_id = "123e4567-e89b-12d3-a456-426614174000"
deal_details = await client.get_opened_deal(deal_id)
if deal_details:
print(f"Deal {deal_details['id']}: {deal_details['asset']} {deal_details['direction']}")
else:
print("Deal not found")
```
"""
deal_json = await self.client.get_opened_deal(id)
if deal_json is None:
return None
return json.loads(deal_json)
async def open_pending_order(
self,
open_type: int,
amount: float,
asset: str,
open_time: Union[int, str],
open_price: float,
timeframe: int,
min_payout: int,
command: int,
) -> Dict:
"""
Opens a pending order on the PocketOption platform.
Args:
open_type (int): The type of the pending order.
amount (float): The amount to trade.
asset (str): The asset symbol (e.g., "EURUSD_otc").
open_time (int | str): The server time to open the trade.
Can be a Unix timestamp (int) or a formatted string "YYYY-MM-DD HH:MM:SS".
open_price (float): The price to open the trade at.
timeframe (int): The duration of the trade in seconds.
min_payout (int): The minimum payout percentage required.
command (int): The trade direction (0 for Call, 1 for Put).
Returns:
Dict: The created pending order details.
"""
# Backward compatibility: If the underlying Rust client still expects an integer
# but we received a string, try to convert it if it's numeric, or fallback to 0.
# This handles cases where the binary extension hasn't been updated to support strings.
actual_open_time = open_time
try:
# We try to call it with the original value first
order = await self.client.open_pending_order(
open_type, amount, asset, actual_open_time, open_price, timeframe, min_payout, command
)
except TypeError as e:
if "object cannot be interpreted as an integer" in str(e) and isinstance(open_time, str):
# Fallback: if it's a string like "0", convert to 0
if open_time == "0":
actual_open_time = 0
else:
# Try to parse Unix timestamp from string if it's just a number
try:
actual_open_time = int(open_time)
except ValueError:
# It's a formatted date string, but the binary wants an int.
# We can't easily convert "YYYY-MM-DD" to timestamp without more info,
# but for the sake of not crashing, we'll try to parse it or use 0.
from datetime import datetime
try:
# PocketOption strings are usually UTC
dt = datetime.strptime(open_time, "%Y-%m-%d %H:%M:%S")
actual_open_time = int(dt.timestamp())
except Exception:
actual_open_time = 0
# Retry with converted integer
order = await self.client.open_pending_order(
open_type, amount, asset, actual_open_time, open_price, timeframe, min_payout, command
)
else:
raise
return json.loads(order)
async def cancel_pending_order(self, ticket: str) -> Dict:
"""
Cancels a pending order by its ticket identifier.
Args:
ticket (str): The unique ticket string identifying the pending order to cancel.
Returns:
Dict: Cancellation result containing:
- ticket: The ticket of the cancelled order
- status: "cancelled"
Raises:
ValueError: If the ticket is invalid
TimeoutError: If the cancellation times out
RuntimeError: If the order cannot be cancelled (e.g., already executed)
Example:
```python
# Cancel a pending order
result = await client.cancel_pending_order("order-ticket-123")
print(f"Cancelled: {result['ticket']}")
```
"""
result = await self.client.cancel_pending_order(ticket)
return json.loads(result)
async def cancel_pending_orders(self, tickets: List[str]) -> Dict:
"""
Cancels multiple pending orders in a single batch operation.
Args:
tickets (List[str]): A list of ticket strings identifying the pending orders to cancel.
Returns:
Dict: Batch cancellation result containing:
- cancelled: List of tickets that were successfully cancelled
- failed: List of tickets that failed to cancel (if any)
Raises:
ValueError: If any ticket is invalid
TimeoutError: If the batch cancellation times out
Note:
Partial success is possible: some orders may be cancelled while others fail.
Example:
```python
# Cancel multiple pending orders
tickets = ["order-1", "order-2", "order-3"]
result = await client.cancel_pending_orders(tickets)
print(f"Cancelled {len(result['cancelled'])} orders")
```
"""
result = await self.client.cancel_pending_orders(tickets)
return json.loads(result)
async def closed_deals(self) -> List[str]:
"""Retrieves a list of all closed/completed deals.
This method returns the ID of all deals that have been completed, including trades
that have expired and reached a final outcome (win, loss, or draw).
Returns:
List[str]: A list of IDs, each representing a closed deal with details obtainable with the `get_closed_deal` method.:
Raises:
ConnectionError: If the client is not connected to the platform
ValueError: If the response format is invalid
Examples:
Basic usage:
```python
async with PocketOptionAsync(ssid) as client:
closed = await client.closed_deals()
closed = [await client.get_closed_deal(deal_id) for deal_id in closed]
for deal in closed:
print(f"Deal {deal['id']}: {deal['result']} (profit: {deal['profit']})")
```
Calculate total profit/loss:
```python
async def calculate_pnl():
async with PocketOptionAsync(ssid) as client:
closed_ids = await client.closed_deals()
closed = [await client.get_closed_deal(deal_id) for deal_id in closed_ids]
total_pnl = sum(d['profit'] for d in closed)
wins = sum(1 for d in closed if d['result'] == 'win')
print(f"Total P/L: {total_pnl}, Win rate: {wins}/{len(closed)}")
```
"""
return json.loads(await self.client.closed_deals())
async def get_closed_deal(self, id: str) -> Optional[Dict]:
"""
Retrieves details of a specific closed deal by its ID.
Args:
id (str): The unique identifier of the closed deal to retrieve
Returns:
Optional[Dict]: The details of the closed deal if found, otherwise None
- id: Unique deal identifier
- asset: Trading asset symbol
- amount: Trade amount
- direction: "buy" or "sell"
- entry_price: Entry price of the trade
- close_price: Closing/expiry price
- expiry: Expiration timestamp
- result: Final outcome ("win", "loss", or "draw")
- profit: Profit/loss amount (positive for win, negative for loss, 0 for draw)
- timestamp: Deal creation and close timestamps
Raises:
ConnectionError: If the client is not connected to the platform
ValueError: If the response format is invalid
Examples:
Fetch specific closed deal details:
```python
async with PocketOptionAsync(ssid) as client:
deal_id = "123e4567-e89b-12d3-a456-426614174000"
deal_details = await client.get_closed_deal(deal_id)
if deal_details:
print(f"Closed Deal {deal_details['id']}: {deal_details['result']} (profit: {deal_details['profit']})")
else:
print("Closed deal not found")
```
"""
deal_json = await self.client.get_closed_deal(id)
if deal_json is None:
return None
return json.loads(deal_json)
async def clear_closed_deals(self) -> None:
"""Removes all closed deals from the client's memory.
This method clears the internal cache/storage of closed deals. After calling
this method, subsequent calls to `closed_deals()` will only return deals
that have been closed after this operation. This is useful for managing
memory when dealing with a large number of historical trades.
Note:
This operation is irreversible. Once cleared, the closed deal history
cannot be recovered through the client. However, the data may still
be available on the server.
Raises:
ConnectionError: If the client is not connected to the platform
RuntimeError: If the clear operation fails on the server
Examples:
Clear old closed deals:
```python
async with PocketOptionAsync(ssid) as client:
# Check current closed deals count
closed = await client.closed_deals()
print(f"Before clear: {len(closed)} closed deals")
# Clear the cache
await client.clear_closed_deals()
# Verify cleared
closed_after = await client.closed_deals()
print(f"After clear: {len(closed_after)} closed deals")
```
Periodic cleanup:
```python
async def periodic_cleanup():
async with PocketOptionAsync(ssid) as client:
# Clear closed deals every hour
while True:
await asyncio.sleep(3600)
await client.clear_closed_deals()
print("Closed deals cache cleared")
```
"""
await self.client.clear_closed_deals()
async def payout(
self, asset: Optional[Union[str, List[str]]] = None
) -> Union[Dict[str, Optional[int]], List[Optional[int]], int, None]:
"""
Retrieves current payout percentages for all assets.
Returns:
dict: Asset payouts mapping:
{
"EURUSD_otc": 85, # 85% payout
"GBPUSD": 82, # 82% payout
...
}
list: If asset is a list, returns a list of payouts for each asset in the same order
int: If asset is a string, returns the payout for that specific asset
none: If asset didn't match and valid asset none will be returned
"""
payout = json.loads(await self.client.payout())
if isinstance(asset, str):
return payout.get(asset)
elif isinstance(asset, list):
return [payout.get(ast) for ast in asset]
else:
return payout
async def active_assets(self) -> List[Dict]:
"""
Retrieves a list of all active assets.
Returns:
List[Dict]: List of active assets, each containing:
- id: Asset ID
- symbol: Asset symbol (e.g., "EURUSD_otc")
- name: Human-readable name
- asset_type: Type of asset (stock, currency, commodity, cryptocurrency, index)
- payout: Payout percentage
- is_otc: Whether this is an OTC asset
- is_active: Whether the asset is currently active for trading
- allowed_candles: List of allowed timeframe durations in seconds
Example:
```python
async with PocketOptionAsync(ssid) as client:
active = await client.active_assets()
for asset in active:
print(f"{asset['symbol']}: {asset['name']} (payout: {asset['payout']}%)")
```
"""
assets_json = await self.client.active_assets()
assets = json.loads(assets_json)
return list(assets.values()) if isinstance(assets, dict) else assets
async def history(self, asset: str, period: int) -> List[Dict]:
"""Retrieves historical price data for an asset.
This method fetches the latest available historical data for the specified asset,
starting from the given period. The returned data format is identical to
`get_candles()`, containing OHLC (Open, High, Low, Close) candle data.
Args:
asset (str): Trading asset symbol (e.g., "EURUSD_otc", "BTCUSD")
period (int): Time period in seconds to fetch historical data from.
For example, period=60 fetches data from the last minute.
Returns:
List[Dict]: A list of dictionaries, each representing a candlestick with:
- time: Candle timestamp (Unix timestamp)
- open: Opening price
- high: Highest price during the period
- low: Lowest price during the period
- close: Closing price
Raises:
ConnectionError: If the client is not connected to the platform
ValueError: If the asset is invalid or the period is not supported
TimeoutError: If the data fetch times out
Examples:
Basic usage - fetch last minute of data:
```python
async with PocketOptionAsync(ssid) as client:
candles = await client.history("EURUSD_otc", 60)
for candle in candles:
print(f"{candle['time']}: O={candle['open']}, C={candle['close']}")
```
Calculate moving average:
```python
async def calculate_ma(asset, period=300):
async with PocketOptionAsync(ssid) as client:
candles = await client.history(asset, period)
if candles:
closes = [c['close'] for c in candles]
ma = sum(closes) / len(closes)
print(f"Simple Moving Average: {ma:.5f}")
```
Note:
This method is similar to `get_candles()` but uses a different API endpoint
and may have different availability or latency characteristics. For advanced
historical data with specific time ranges, consider using `get_candles_advanced()`.
"""
return json.loads(await self.client.history(asset, period))
async def compile_candles(self, asset: str, custom_period: int, lookback_period: int) -> List[Dict]:
"""Compiles custom candlesticks from raw tick history.
This method fetches raw tick data over the specified lookback period and
aggregates it into custom-sized candles. This enables non-standard timeframes
like 20 seconds, 40 seconds, 90 seconds, etc.
Args:
asset (str): Trading asset symbol (e.g., "EURUSD_otc")
custom_period (int): Desired candle duration in seconds (e.g., 20, 40, 90)
lookback_period (int): Number of seconds of tick history to fetch.
This determines the time range from which ticks are collected.
Returns:
List[Dict]: A list of dictionaries, each representing a compiled candlestick:
- time: Candle timestamp (Unix timestamp, aligned to period boundaries)
- open: Opening price
- high: Highest price during the period
- low: Lowest price during the period
- close: Closing price
Raises:
ConnectionError: If the client is not connected
ValueError: If the asset is invalid or periods are zero/negative
TimeoutError: If tick fetch or compilation times out
Example:
```python
async with PocketOptionAsync(ssid) as client:
# Get 20-second candles from last 5 minutes
candles = await client.compile_candles("EURUSD_otc", 20, 300)
for candle in candles:
print(f"{candle['time']}: O={candle['open']}, C={candle['close']}")
```
Note:
- This is a compute-intensive operation as it fetches and processes raw ticks.
- For standard timeframes, use `candles()` or `get_candles()` for better efficiency.
"""
if not isinstance(custom_period, int) or custom_period <= 0:
raise ValueError("custom_period must be a positive integer")
if not isinstance(lookback_period, int) or lookback_period <= 0:
raise ValueError("lookback_period must be a positive integer")
return json.loads(await self.client.compile_candles(asset, custom_period, lookback_period))
async def subscribe_symbol(self, asset: str) -> AsyncSubscription:
"""Subscribe to real-time raw price updates for an asset.
Returns an async iterator yielding JSON-parsed price updates.
"""
return AsyncSubscription(await self.client.subscribe_symbol(asset))
async def subscribe_symbol_chunked(self, asset: str, chunk_size: int) -> AsyncSubscription:
"""Subscribe with chunked candle aggregation (n raw ticks per candle)."""
return AsyncSubscription(await self.client.subscribe_symbol_chunked(asset, chunk_size))