pocket-option 0.1.0__tar.gz
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.
- pocket_option-0.1.0/LICENSE +21 -0
- pocket_option-0.1.0/PKG-INFO +106 -0
- pocket_option-0.1.0/README.md +84 -0
- pocket_option-0.1.0/pocket_option/__init__.py +3 -0
- pocket_option-0.1.0/pocket_option/client.py +271 -0
- pocket_option-0.1.0/pocket_option/constants.py +52 -0
- pocket_option-0.1.0/pocket_option/contrib/__init__.py +0 -0
- pocket_option-0.1.0/pocket_option/contrib/candles.py +133 -0
- pocket_option-0.1.0/pocket_option/contrib/deals.py +265 -0
- pocket_option-0.1.0/pocket_option/generated_client.py +262 -0
- pocket_option-0.1.0/pocket_option/middleware.py +19 -0
- pocket_option-0.1.0/pocket_option/middlewares.py +40 -0
- pocket_option-0.1.0/pocket_option/models.py +320 -0
- pocket_option-0.1.0/pocket_option/types.py +31 -0
- pocket_option-0.1.0/pocket_option/utils.py +73 -0
- pocket_option-0.1.0/pyproject.toml +111 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yury "lordralinc" Yushmanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pocket-option
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async API client for Pocket Option platform
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Yury "lordralinc" Yushmanov
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Provides-Extra: ujson
|
|
13
|
+
Requires-Dist: aiohttp[speedups] (>=3.13.1,<4.0.0)
|
|
14
|
+
Requires-Dist: pydantic (>=2.12.3,<3.0.0)
|
|
15
|
+
Requires-Dist: python-socketio[asyncio-client] (>=5.14.2,<6.0.0)
|
|
16
|
+
Requires-Dist: pytz (>=2025.2,<2026.0)
|
|
17
|
+
Requires-Dist: ujson (>=5.11.0,<6.0.0) ; extra == "ujson"
|
|
18
|
+
Project-URL: Homepage, https://github.com/lordralinc/pocket_option
|
|
19
|
+
Project-URL: Issues, https://github.com/lordralinc/pocket_option/issues
|
|
20
|
+
Project-URL: Repository, https://github.com/lordralinc/pocket_option
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# ⚡ PocketOption API SDK (Unofficial)
|
|
24
|
+
|
|
25
|
+
Асинхронный **Python-SDK для взаимодействия с PocketOption API** (неофициальный).
|
|
26
|
+
|
|
27
|
+
Полностью типизирован, построен на `pydantic`, с поддержкой middleware, событий.
|
|
28
|
+
|
|
29
|
+
>⚠️ Проект не связан с PocketOption. Предназначен для интеграций, анализа и автоматизации.
|
|
30
|
+
|
|
31
|
+
> Поддерживает Python 3.13+ и полностью асинхронен (`asyncio` + `aiohttp`).
|
|
32
|
+
|
|
33
|
+
> ## Предупреждение о рисках:
|
|
34
|
+
> Инвестирование в финансовые продукты сопряжено с рисками. Прошлые результаты не гарантируют будущую доходность, а стоимость активов может изменяться в зависимости от рыночных условий и колебаний базовых инструментов. Любые прогнозы или иллюстрации приведены исключительно для справки и не являются гарантией результата. Этот проект не является приглашением или рекомендацией к инвестированию. Перед инвестированием проконсультируйтесь с финансовыми, юридическими и налоговыми специалистами и решите, подходит ли данный продукт вашим целям, допустимому уровню риска и текущей ситуации.
|
|
35
|
+
|
|
36
|
+
> P.S. У них демо прикольное, чисто позалипать кайф
|
|
37
|
+
|
|
38
|
+
## 🚀 Возможности
|
|
39
|
+
|
|
40
|
+
- 🔌 Подключение к WebSocket-API PocketOption (через `socket io`)
|
|
41
|
+
|
|
42
|
+
- 🔐 Авторизация по активной сессии
|
|
43
|
+
|
|
44
|
+
- 💹 Управление ордерами и сделками (демо / реальный счёт)
|
|
45
|
+
|
|
46
|
+
- 📊 Подписка на рыночные потоки
|
|
47
|
+
|
|
48
|
+
- 💾 Встроенные in-memory-хранилища (`MemoryCandleStorage`, `MemoryDealsStorage`)
|
|
49
|
+
|
|
50
|
+
- ⚙️ Middleware-цепочка для перехвата событий и запросов
|
|
51
|
+
|
|
52
|
+
- 💬 Событийная модель с декораторами (`@client.on.*`)
|
|
53
|
+
|
|
54
|
+
- ✅ Строгая типизация
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## ⚙️ Пример использования
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import asyncio
|
|
61
|
+
import os
|
|
62
|
+
from pocket_option import PocketOptionClient
|
|
63
|
+
from pocket_option.models import Asset, OrderAction
|
|
64
|
+
from pocket_option.constants import Regions
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def on_update_stream(assets: list[UpdateStreamItem]):
|
|
69
|
+
print("Assets updated: ", assets)
|
|
70
|
+
|
|
71
|
+
async def main():
|
|
72
|
+
client = PocketOptionClient()
|
|
73
|
+
client.on.update_stream(on_update_stream)
|
|
74
|
+
|
|
75
|
+
deals = MemoryDealsStorage(client)
|
|
76
|
+
|
|
77
|
+
await client.connect(Regions.DEMO)
|
|
78
|
+
await client.emit.auth(AuthorizationData.model_validate({
|
|
79
|
+
"session": os.environ["PO_SESSION"],
|
|
80
|
+
"isDemo": 1,
|
|
81
|
+
"uid": int(os.environ["PO_UID"]),
|
|
82
|
+
"platform": 2,
|
|
83
|
+
"isFastHistory": True,
|
|
84
|
+
"isOptimized": True,
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
deal = await deals.open_deal(
|
|
88
|
+
asset=Asset.AUDCAD_otc,
|
|
89
|
+
amount=10,
|
|
90
|
+
action=OrderAction.CALL,
|
|
91
|
+
is_demo=1,
|
|
92
|
+
option_type=100,
|
|
93
|
+
time=60,
|
|
94
|
+
)
|
|
95
|
+
print("✅ Opened deal:", deal)
|
|
96
|
+
result = await deals.check_deal_result(wait_time=60, deal=deal)
|
|
97
|
+
print("✅ Deal result:", result)
|
|
98
|
+
|
|
99
|
+
asyncio.run(main())
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## 📜 Лицензия
|
|
105
|
+
|
|
106
|
+
**MIT License** — делай что хочешь, но на свой страх и риск.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ⚡ PocketOption API SDK (Unofficial)
|
|
2
|
+
|
|
3
|
+
Асинхронный **Python-SDK для взаимодействия с PocketOption API** (неофициальный).
|
|
4
|
+
|
|
5
|
+
Полностью типизирован, построен на `pydantic`, с поддержкой middleware, событий.
|
|
6
|
+
|
|
7
|
+
>⚠️ Проект не связан с PocketOption. Предназначен для интеграций, анализа и автоматизации.
|
|
8
|
+
|
|
9
|
+
> Поддерживает Python 3.13+ и полностью асинхронен (`asyncio` + `aiohttp`).
|
|
10
|
+
|
|
11
|
+
> ## Предупреждение о рисках:
|
|
12
|
+
> Инвестирование в финансовые продукты сопряжено с рисками. Прошлые результаты не гарантируют будущую доходность, а стоимость активов может изменяться в зависимости от рыночных условий и колебаний базовых инструментов. Любые прогнозы или иллюстрации приведены исключительно для справки и не являются гарантией результата. Этот проект не является приглашением или рекомендацией к инвестированию. Перед инвестированием проконсультируйтесь с финансовыми, юридическими и налоговыми специалистами и решите, подходит ли данный продукт вашим целям, допустимому уровню риска и текущей ситуации.
|
|
13
|
+
|
|
14
|
+
> P.S. У них демо прикольное, чисто позалипать кайф
|
|
15
|
+
|
|
16
|
+
## 🚀 Возможности
|
|
17
|
+
|
|
18
|
+
- 🔌 Подключение к WebSocket-API PocketOption (через `socket io`)
|
|
19
|
+
|
|
20
|
+
- 🔐 Авторизация по активной сессии
|
|
21
|
+
|
|
22
|
+
- 💹 Управление ордерами и сделками (демо / реальный счёт)
|
|
23
|
+
|
|
24
|
+
- 📊 Подписка на рыночные потоки
|
|
25
|
+
|
|
26
|
+
- 💾 Встроенные in-memory-хранилища (`MemoryCandleStorage`, `MemoryDealsStorage`)
|
|
27
|
+
|
|
28
|
+
- ⚙️ Middleware-цепочка для перехвата событий и запросов
|
|
29
|
+
|
|
30
|
+
- 💬 Событийная модель с декораторами (`@client.on.*`)
|
|
31
|
+
|
|
32
|
+
- ✅ Строгая типизация
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## ⚙️ Пример использования
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
import os
|
|
40
|
+
from pocket_option import PocketOptionClient
|
|
41
|
+
from pocket_option.models import Asset, OrderAction
|
|
42
|
+
from pocket_option.constants import Regions
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def on_update_stream(assets: list[UpdateStreamItem]):
|
|
47
|
+
print("Assets updated: ", assets)
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
client = PocketOptionClient()
|
|
51
|
+
client.on.update_stream(on_update_stream)
|
|
52
|
+
|
|
53
|
+
deals = MemoryDealsStorage(client)
|
|
54
|
+
|
|
55
|
+
await client.connect(Regions.DEMO)
|
|
56
|
+
await client.emit.auth(AuthorizationData.model_validate({
|
|
57
|
+
"session": os.environ["PO_SESSION"],
|
|
58
|
+
"isDemo": 1,
|
|
59
|
+
"uid": int(os.environ["PO_UID"]),
|
|
60
|
+
"platform": 2,
|
|
61
|
+
"isFastHistory": True,
|
|
62
|
+
"isOptimized": True,
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
deal = await deals.open_deal(
|
|
66
|
+
asset=Asset.AUDCAD_otc,
|
|
67
|
+
amount=10,
|
|
68
|
+
action=OrderAction.CALL,
|
|
69
|
+
is_demo=1,
|
|
70
|
+
option_type=100,
|
|
71
|
+
time=60,
|
|
72
|
+
)
|
|
73
|
+
print("✅ Opened deal:", deal)
|
|
74
|
+
result = await deals.check_deal_result(wait_time=60, deal=deal)
|
|
75
|
+
print("✅ Deal result:", result)
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## 📜 Лицензия
|
|
83
|
+
|
|
84
|
+
**MIT License** — делай что хочешь, но на свой страх и риск.
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import collections.abc
|
|
3
|
+
import logging
|
|
4
|
+
import typing
|
|
5
|
+
from inspect import isclass
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
import pydantic
|
|
9
|
+
import socketio
|
|
10
|
+
|
|
11
|
+
from pocket_option.constants import DEFAULT_ORIGIN, DEFAULT_USER_AGENT
|
|
12
|
+
from pocket_option.middleware import Middleware
|
|
13
|
+
from pocket_option.middlewares import FixTypesOnMiddleware, MakeJsonOnMiddleware
|
|
14
|
+
from pocket_option.utils import get_json_function
|
|
15
|
+
|
|
16
|
+
if typing.TYPE_CHECKING:
|
|
17
|
+
from pocket_option import models
|
|
18
|
+
from pocket_option.types import EmitCallback, JsonFunction, JsonValue, SIOEventListener
|
|
19
|
+
|
|
20
|
+
__all__ = ("BasePocketOptionClient",)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BasePocketOptionClient:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
on_middlewares: "list[Middleware] | None" = None,
|
|
29
|
+
*,
|
|
30
|
+
reconnection: bool = True,
|
|
31
|
+
reconnection_attempts: int = 0,
|
|
32
|
+
reconnection_delay: float = 1.0,
|
|
33
|
+
reconnection_delay_max: float = 5.0,
|
|
34
|
+
randomization_factor: float = 0.5,
|
|
35
|
+
logger: bool = False,
|
|
36
|
+
engineio_logger: bool = False,
|
|
37
|
+
json: "JsonFunction | None" = None,
|
|
38
|
+
handle_sigint: bool = True,
|
|
39
|
+
request_timeout: float = 5,
|
|
40
|
+
http_session: aiohttp.ClientSession | None = None,
|
|
41
|
+
ssl_verify: bool = True,
|
|
42
|
+
websocket_extra_options: dict | None = None,
|
|
43
|
+
timestamp_requests: bool = False,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Initializes the Socket.IO client wrapper with middleware and connection options.
|
|
46
|
+
|
|
47
|
+
:param on_middlewares:
|
|
48
|
+
A list of middlewares executed on incoming Socket.IO events.
|
|
49
|
+
Each middleware can modify or intercept event data before the callback is called.
|
|
50
|
+
By default, the following middlewares are applied:
|
|
51
|
+
- `MakeJsonOnMiddleware()` — parses JSON data into Python objects.
|
|
52
|
+
- `FixTypesOnMiddleware()` — normalizes data types for consistency.
|
|
53
|
+
|
|
54
|
+
:param emit_middlewares:
|
|
55
|
+
A list of middlewares executed before emitting events to the server.
|
|
56
|
+
Each middleware can modify the outgoing payload.
|
|
57
|
+
Defaults to an empty list.
|
|
58
|
+
|
|
59
|
+
:param reconnection:
|
|
60
|
+
Enables or disables automatic reconnection when the connection is lost.
|
|
61
|
+
Defaults to `True`.
|
|
62
|
+
|
|
63
|
+
:param reconnection_attempts:
|
|
64
|
+
The maximum number of reconnection attempts.
|
|
65
|
+
A value of `0` means unlimited retries.
|
|
66
|
+
|
|
67
|
+
:param reconnection_delay:
|
|
68
|
+
Initial delay (in seconds) before attempting to reconnect.
|
|
69
|
+
Defaults to `1.0`.
|
|
70
|
+
|
|
71
|
+
:param reconnection_delay_max:
|
|
72
|
+
Maximum delay (in seconds) between reconnection attempts.
|
|
73
|
+
Defaults to `5.0`.
|
|
74
|
+
|
|
75
|
+
:param randomization_factor:
|
|
76
|
+
A factor between 0 and 1 used to randomize the reconnection delay to prevent
|
|
77
|
+
simultaneous reconnects.
|
|
78
|
+
Defaults to `0.5`.
|
|
79
|
+
|
|
80
|
+
:param logger:
|
|
81
|
+
Enables logging for the Socket.IO client.
|
|
82
|
+
Defaults to `False`.
|
|
83
|
+
|
|
84
|
+
:param engineio_logger:
|
|
85
|
+
Enables logging for the underlying Engine.IO layer.
|
|
86
|
+
Defaults to `False`.
|
|
87
|
+
|
|
88
|
+
:param json:
|
|
89
|
+
Custom JSON serialization/deserialization functions.
|
|
90
|
+
If not provided, a default implementation will be used.
|
|
91
|
+
|
|
92
|
+
:param handle_sigint:
|
|
93
|
+
If `True`, the client will handle SIGINT (Ctrl+C) for clean shutdown.
|
|
94
|
+
Defaults to `True`.
|
|
95
|
+
|
|
96
|
+
:param request_timeout:
|
|
97
|
+
The timeout (in seconds) for HTTP requests during the Socket.IO handshake.
|
|
98
|
+
Defaults to `5`.
|
|
99
|
+
|
|
100
|
+
:param http_session:
|
|
101
|
+
Optional existing `aiohttp.ClientSession` instance to reuse for HTTP requests.
|
|
102
|
+
If `None`, a new session is created internally.
|
|
103
|
+
|
|
104
|
+
:param ssl_verify:
|
|
105
|
+
Whether to verify SSL certificates for secure connections.
|
|
106
|
+
Defaults to `True`.
|
|
107
|
+
|
|
108
|
+
:param websocket_extra_options:
|
|
109
|
+
Optional dictionary of additional parameters for the WebSocket connection.
|
|
110
|
+
Useful for setting headers or specific connection parameters.
|
|
111
|
+
|
|
112
|
+
:param timestamp_requests:
|
|
113
|
+
Whether to append a timestamp to each request for caching avoidance.
|
|
114
|
+
Defaults to `False`.
|
|
115
|
+
"""
|
|
116
|
+
self.middlewares = on_middlewares or [MakeJsonOnMiddleware(), FixTypesOnMiddleware()]
|
|
117
|
+
self.json = json or get_json_function()
|
|
118
|
+
self.sio = socketio.AsyncClient(
|
|
119
|
+
reconnection=reconnection,
|
|
120
|
+
reconnection_attempts=reconnection_attempts,
|
|
121
|
+
reconnection_delay=reconnection_delay, # pyright: ignore[reportArgumentType]
|
|
122
|
+
reconnection_delay_max=reconnection_delay_max, # pyright: ignore[reportArgumentType]
|
|
123
|
+
randomization_factor=randomization_factor,
|
|
124
|
+
logger=logger,
|
|
125
|
+
serializer="default",
|
|
126
|
+
json=self.json,
|
|
127
|
+
handle_sigint=handle_sigint,
|
|
128
|
+
request_timeout=request_timeout,
|
|
129
|
+
http_session=http_session,
|
|
130
|
+
ssl_verify=ssl_verify,
|
|
131
|
+
websocket_extra_options=websocket_extra_options,
|
|
132
|
+
timestamp_requests=timestamp_requests,
|
|
133
|
+
engineio_logger=engineio_logger,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def get_auth_from_packet(self, packet: str) -> "models.AuthorizationData":
|
|
137
|
+
packet = packet.removeprefix("42")
|
|
138
|
+
json_packet = self.json.loads(packet)
|
|
139
|
+
return typing.cast("models.AuthorizationData", json_packet)
|
|
140
|
+
|
|
141
|
+
def add_middleware(self, middleware: Middleware) -> None:
|
|
142
|
+
self.middlewares.append(middleware)
|
|
143
|
+
|
|
144
|
+
async def _get_real_value[T](
|
|
145
|
+
self,
|
|
146
|
+
value: T
|
|
147
|
+
| None
|
|
148
|
+
| collections.abc.Callable[[], T]
|
|
149
|
+
| collections.abc.Callable[[], collections.abc.Coroutine[None, None, T]],
|
|
150
|
+
) -> T | None:
|
|
151
|
+
if callable(value):
|
|
152
|
+
result = value()
|
|
153
|
+
if asyncio.iscoroutine(result):
|
|
154
|
+
return await result
|
|
155
|
+
return result # type: ignore
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
async def wait(self):
|
|
159
|
+
return await self.sio.wait()
|
|
160
|
+
|
|
161
|
+
async def disconnect(self) -> None:
|
|
162
|
+
return await self.sio.disconnect()
|
|
163
|
+
|
|
164
|
+
async def shutdown(self) -> None:
|
|
165
|
+
return await self.sio.shutdown()
|
|
166
|
+
|
|
167
|
+
async def sleep(self, seconds: float = 0) -> None:
|
|
168
|
+
return await self.sio.sleep(seconds=seconds) # type: ignore
|
|
169
|
+
|
|
170
|
+
async def connect(
|
|
171
|
+
self,
|
|
172
|
+
url: str,
|
|
173
|
+
headers: dict[str, str] | collections.abc.Callable[[], dict[str, str]] | None = None,
|
|
174
|
+
auth: "models.AuthorizationData | None" = None,
|
|
175
|
+
wait: bool = True,
|
|
176
|
+
wait_timeout: float = 1,
|
|
177
|
+
retry: bool = False,
|
|
178
|
+
):
|
|
179
|
+
headers = await self._get_real_value(headers) or {}
|
|
180
|
+
headers.setdefault("Origin", DEFAULT_ORIGIN)
|
|
181
|
+
headers.setdefault("User-Agent", DEFAULT_USER_AGENT)
|
|
182
|
+
return await self.sio.connect(
|
|
183
|
+
url,
|
|
184
|
+
headers=headers,
|
|
185
|
+
auth=auth,
|
|
186
|
+
transports=["websocket"],
|
|
187
|
+
namespaces=["/"],
|
|
188
|
+
socketio_path="socket.io",
|
|
189
|
+
wait=wait,
|
|
190
|
+
wait_timeout=wait_timeout, # type: ignore
|
|
191
|
+
retry=retry,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@typing.overload
|
|
195
|
+
def add_on(
|
|
196
|
+
self,
|
|
197
|
+
event: str,
|
|
198
|
+
handler: None = ...,
|
|
199
|
+
*,
|
|
200
|
+
model: type[pydantic.BaseModel] | pydantic.TypeAdapter | None = ...,
|
|
201
|
+
) -> "typing.Callable[[SIOEventListener], None]": ...
|
|
202
|
+
@typing.overload
|
|
203
|
+
def add_on(
|
|
204
|
+
self,
|
|
205
|
+
event: str,
|
|
206
|
+
handler: "SIOEventListener",
|
|
207
|
+
*,
|
|
208
|
+
model: type[pydantic.BaseModel] | pydantic.TypeAdapter | None = ...,
|
|
209
|
+
) -> None: ...
|
|
210
|
+
|
|
211
|
+
def add_on(
|
|
212
|
+
self,
|
|
213
|
+
event: str,
|
|
214
|
+
handler: "SIOEventListener | None" = None,
|
|
215
|
+
*,
|
|
216
|
+
model: type[pydantic.BaseModel] | pydantic.TypeAdapter | None = None,
|
|
217
|
+
) -> "None | typing.Callable[[SIOEventListener], None]":
|
|
218
|
+
def _get_data(d: "JsonValue | None"):
|
|
219
|
+
if d and isinstance(d, dict) and model and isclass(model) and issubclass(model, pydantic.BaseModel):
|
|
220
|
+
return model.model_validate(d)
|
|
221
|
+
if d and isinstance(model, pydantic.TypeAdapter):
|
|
222
|
+
return model.validate_python(d)
|
|
223
|
+
return d
|
|
224
|
+
|
|
225
|
+
if handler is None:
|
|
226
|
+
|
|
227
|
+
def set_handler(_handler: "SIOEventListener"):
|
|
228
|
+
async def wrapper(data: "JsonValue | None"):
|
|
229
|
+
for middleware in self.middlewares:
|
|
230
|
+
data = await middleware.on(event, data)
|
|
231
|
+
new_data = _get_data(data)
|
|
232
|
+
logger.debug("New event '%s' with data %r", event, new_data)
|
|
233
|
+
result = _handler(new_data)
|
|
234
|
+
if asyncio.iscoroutine(result):
|
|
235
|
+
await result
|
|
236
|
+
|
|
237
|
+
self.sio.on(event, handler=wrapper)
|
|
238
|
+
|
|
239
|
+
return set_handler
|
|
240
|
+
|
|
241
|
+
async def _handler(data: "JsonValue | None"):
|
|
242
|
+
for middleware in self.middlewares:
|
|
243
|
+
data = await middleware.on(event, data)
|
|
244
|
+
new_data = _get_data(data)
|
|
245
|
+
logger.debug("New event '%s' with data %r", event, new_data)
|
|
246
|
+
result = handler(new_data)
|
|
247
|
+
if asyncio.iscoroutine(result):
|
|
248
|
+
await result
|
|
249
|
+
|
|
250
|
+
return self.sio.on(event, handler=_handler)
|
|
251
|
+
|
|
252
|
+
async def send(
|
|
253
|
+
self,
|
|
254
|
+
event: str,
|
|
255
|
+
data: "JsonValue | pydantic.BaseModel | None" = None,
|
|
256
|
+
callback: "EmitCallback[JsonValue] | None" = None,
|
|
257
|
+
) -> None:
|
|
258
|
+
if isinstance(data, pydantic.BaseModel):
|
|
259
|
+
data = data.model_dump(mode="json", by_alias=True)
|
|
260
|
+
if isinstance(data, list):
|
|
261
|
+
data = [
|
|
262
|
+
it.model_dump(mode="json", by_alias=True) if isinstance(it, pydantic.BaseModel) else it for it in data
|
|
263
|
+
]
|
|
264
|
+
for middleware in self.middlewares:
|
|
265
|
+
event, data, callback = await middleware.emit(event, data=data, callback=callback)
|
|
266
|
+
logger.debug(
|
|
267
|
+
"Emitting event '%s' with data %r",
|
|
268
|
+
event,
|
|
269
|
+
data if event != "auth" else {**typing.cast("dict[str, JsonValue]", data), "session": "***"},
|
|
270
|
+
)
|
|
271
|
+
return await self.sio.emit(event=event, data=data, callback=callback)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
__all__ = (
|
|
2
|
+
"API_LIMITS_MAX_CONCURRENT_ORDERS",
|
|
3
|
+
"API_LIMITS_MAX_DURATION",
|
|
4
|
+
"API_LIMITS_MAX_ORDER_AMOUNT",
|
|
5
|
+
"API_LIMITS_MIN_DURATION",
|
|
6
|
+
"API_LIMITS_MIN_ORDER_AMOUNT",
|
|
7
|
+
"API_LIMITS_RATE_LIMIT",
|
|
8
|
+
"DEFAULT_ORIGIN",
|
|
9
|
+
"DEFAULT_USER_AGENT",
|
|
10
|
+
"MAX_INT_32",
|
|
11
|
+
"TIMESTAMP_OFFSET",
|
|
12
|
+
"Regions",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
API_LIMITS_MIN_ORDER_AMOUNT = 1
|
|
17
|
+
API_LIMITS_MAX_ORDER_AMOUNT = 50_000
|
|
18
|
+
API_LIMITS_MIN_DURATION = 5
|
|
19
|
+
API_LIMITS_MAX_DURATION = 43_200
|
|
20
|
+
API_LIMITS_MAX_CONCURRENT_ORDERS = 10
|
|
21
|
+
API_LIMITS_RATE_LIMIT = 100
|
|
22
|
+
MAX_INT_32 = 2_147_483_647
|
|
23
|
+
TIMESTAMP_OFFSET = -7200
|
|
24
|
+
DEFAULT_ORIGIN = "https://m.pocketoption.com"
|
|
25
|
+
DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Regions:
|
|
29
|
+
UNITED_STATES_NORTH = "wss://api-us-north.po.market"
|
|
30
|
+
UNITED_STATES_SOUTH = "wss://api-us-south.po.market"
|
|
31
|
+
EUROPA = "wss://api-eu.po.market"
|
|
32
|
+
ASIA = "wss://api-asia.po.market"
|
|
33
|
+
|
|
34
|
+
UNITED_STATES_2 = "wss://api-us2.po.market"
|
|
35
|
+
UNITED_STATES_3 = "wss://api-us3.po.market"
|
|
36
|
+
UNITED_STATES_4 = "wss://api-us4.po.market"
|
|
37
|
+
|
|
38
|
+
FRANCE_1 = "wss://api-fr.po.market"
|
|
39
|
+
FRANCE_2 = "wss://api-fr2.po.market"
|
|
40
|
+
RUSSIA = "wss://api-msk.po.market"
|
|
41
|
+
INDIA = "wss://api-in.po.market"
|
|
42
|
+
FINLAND = "wss://api-fin.po.market"
|
|
43
|
+
|
|
44
|
+
SEYCHELLES = "wss://api-sc.po.market"
|
|
45
|
+
HONGKONG = "wss://api-hk.po.market"
|
|
46
|
+
|
|
47
|
+
SERVER_1 = "wss://api-spb.po.market"
|
|
48
|
+
SERVER_2 = "wss://api-l.po.market"
|
|
49
|
+
SERVER_3 = "wss://api-c.po.market"
|
|
50
|
+
|
|
51
|
+
DEMO = "wss://demo-api-eu.po.market"
|
|
52
|
+
DEMO_2 = "wss://try-demo-eu.po.market"
|
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import collections.abc
|
|
3
|
+
import datetime
|
|
4
|
+
import math
|
|
5
|
+
import typing
|
|
6
|
+
from collections import defaultdict, deque
|
|
7
|
+
|
|
8
|
+
import pydantic
|
|
9
|
+
import pytz
|
|
10
|
+
|
|
11
|
+
from pocket_option.generated_client import PocketOptionClient
|
|
12
|
+
from pocket_option.models import Asset, UpdateStreamItem
|
|
13
|
+
from pocket_option.utils import append_or_replace
|
|
14
|
+
|
|
15
|
+
if typing.TYPE_CHECKING:
|
|
16
|
+
from pocket_option.generated_client import PocketOptionClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Candle(pydantic.BaseModel):
|
|
20
|
+
asset: Asset
|
|
21
|
+
timestamp: datetime.datetime
|
|
22
|
+
timeframe: int
|
|
23
|
+
open: float
|
|
24
|
+
low: float
|
|
25
|
+
high: float
|
|
26
|
+
close: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CandleStorage(abc.ABC):
|
|
30
|
+
def __init__(self, client: "PocketOptionClient") -> None:
|
|
31
|
+
self.client = client
|
|
32
|
+
|
|
33
|
+
self.client.on.update_stream(self._on_update_stream)
|
|
34
|
+
|
|
35
|
+
async def _on_update_stream(self, items: list[UpdateStreamItem]) -> None:
|
|
36
|
+
await self.add_item_bulk(items)
|
|
37
|
+
|
|
38
|
+
async def add_candle(self, candle: Candle) -> None:
|
|
39
|
+
await self.add_item_bulk(
|
|
40
|
+
[
|
|
41
|
+
UpdateStreamItem(asset=candle.asset, timestamp=candle.timestamp.timestamp(), value=candle.open),
|
|
42
|
+
UpdateStreamItem(asset=candle.asset, timestamp=candle.timestamp.timestamp() + 0.01, value=candle.low),
|
|
43
|
+
UpdateStreamItem(asset=candle.asset, timestamp=candle.timestamp.timestamp() + 0.02, value=candle.high),
|
|
44
|
+
UpdateStreamItem(
|
|
45
|
+
asset=candle.asset,
|
|
46
|
+
timestamp=candle.timestamp.timestamp() + (candle.timeframe - 0.01),
|
|
47
|
+
value=candle.close,
|
|
48
|
+
),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@abc.abstractmethod
|
|
53
|
+
async def add_item(self, item: UpdateStreamItem): ...
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
async def add_item_bulk(self, items: list[UpdateStreamItem]): ...
|
|
56
|
+
@abc.abstractmethod
|
|
57
|
+
async def get_items(
|
|
58
|
+
self,
|
|
59
|
+
asset: Asset,
|
|
60
|
+
*,
|
|
61
|
+
start: datetime.datetime | None = None,
|
|
62
|
+
end: datetime.datetime | None = None,
|
|
63
|
+
count: int | None = None,
|
|
64
|
+
) -> collections.abc.Iterable[UpdateStreamItem]: ...
|
|
65
|
+
|
|
66
|
+
async def get_candles(
|
|
67
|
+
self,
|
|
68
|
+
asset: Asset,
|
|
69
|
+
timeframe: int = 5,
|
|
70
|
+
*,
|
|
71
|
+
start: datetime.datetime | None = None,
|
|
72
|
+
end: datetime.datetime | None = None,
|
|
73
|
+
count: int | None = None,
|
|
74
|
+
) -> collections.abc.Iterable[Candle]:
|
|
75
|
+
items = await self.get_items(asset, start=start, end=end, count=count)
|
|
76
|
+
|
|
77
|
+
buckets: dict[int, list[UpdateStreamItem]] = defaultdict(list)
|
|
78
|
+
for item in items:
|
|
79
|
+
ts_bucket = math.floor(item.timestamp / timeframe) * timeframe
|
|
80
|
+
buckets[ts_bucket].append(item)
|
|
81
|
+
candles = []
|
|
82
|
+
|
|
83
|
+
for ts_bucket in sorted(buckets):
|
|
84
|
+
group = buckets[ts_bucket]
|
|
85
|
+
values = [i.value for i in group]
|
|
86
|
+
candle = Candle(
|
|
87
|
+
asset=asset,
|
|
88
|
+
timestamp=datetime.datetime.fromtimestamp(ts_bucket, tz=pytz.UTC),
|
|
89
|
+
timeframe=timeframe,
|
|
90
|
+
open=values[0],
|
|
91
|
+
close=values[-1],
|
|
92
|
+
high=max(values),
|
|
93
|
+
low=min(values),
|
|
94
|
+
)
|
|
95
|
+
candles.append(candle)
|
|
96
|
+
|
|
97
|
+
return candles
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MemoryCandleStorage(CandleStorage):
|
|
101
|
+
def __init__(self, client: "PocketOptionClient") -> None:
|
|
102
|
+
super().__init__(client)
|
|
103
|
+
self._max_len = 10_000
|
|
104
|
+
self._storage: dict[Asset, deque[UpdateStreamItem]] = defaultdict(lambda: deque([], 10_000))
|
|
105
|
+
|
|
106
|
+
def set_max_len(self, _max_len: int):
|
|
107
|
+
self._storage = defaultdict(lambda: deque([], _max_len))
|
|
108
|
+
|
|
109
|
+
async def add_item(self, item: UpdateStreamItem):
|
|
110
|
+
self._storage[item.asset] = append_or_replace(self._storage[item.asset], item, ["asset", "timestamp"])
|
|
111
|
+
|
|
112
|
+
async def add_item_bulk(self, items: list[UpdateStreamItem]):
|
|
113
|
+
for it in items:
|
|
114
|
+
await self.add_item(it)
|
|
115
|
+
|
|
116
|
+
async def get_items(
|
|
117
|
+
self,
|
|
118
|
+
asset: Asset,
|
|
119
|
+
*,
|
|
120
|
+
start: datetime.datetime | None = None,
|
|
121
|
+
end: datetime.datetime | None = None,
|
|
122
|
+
count: int | None = None,
|
|
123
|
+
) -> collections.abc.Iterable[UpdateStreamItem]:
|
|
124
|
+
items = self._storage.get(asset, [])
|
|
125
|
+
if start:
|
|
126
|
+
items = [i for i in items if i.timestamp >= start.timestamp()]
|
|
127
|
+
if end:
|
|
128
|
+
items = [i for i in items if i.timestamp <= end.timestamp()]
|
|
129
|
+
items = list(items)
|
|
130
|
+
items.sort(key=lambda i: i.timestamp)
|
|
131
|
+
if count is not None:
|
|
132
|
+
items = items[-count:]
|
|
133
|
+
return items
|