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.
@@ -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,3 @@
1
+ from .generated_client import PocketOptionClient
2
+
3
+ __all__ = ("PocketOptionClient",)
@@ -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