Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore.tmp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
settings/
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ readme = "README.md"
packages = [{ include = "pyquotex" }]
requires-python = ">=3.12,<4.0"

[tool.poetry]
include = [
{ path = "pyquotex/py.typed", format = ["sdist", "wheel"] },
]

dependencies = [
"websockets (>=12.0)",
"httpx (>=0.27.0,<1.0.0)",
Expand Down
25 changes: 25 additions & 0 deletions pyquotex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
"""A python wrapper for Quotex API."""
import logging

from .types import (
AssetInfo,
Balance,
Candle,
ProfileInfo,
ReconnectPolicy,
Subscription,
TradeDirection,
TradeResult,
TradeStatus,
)


def _prepare_logging() -> None:
"""Prepare logger for module Quotex API."""
Expand All @@ -14,3 +26,16 @@ def _prepare_logging() -> None:


_prepare_logging()


__all__ = [
"AssetInfo",
"Balance",
"Candle",
"ProfileInfo",
"ReconnectPolicy",
"Subscription",
"TradeDirection",
"TradeResult",
"TradeStatus",
]
3 changes: 2 additions & 1 deletion pyquotex/_api/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ async def connect(self) -> tuple[bool, str]:
resource_path=self.resource_path,
user_data_dir=self.user_data_dir,
proxies=self.proxies,
on_otp_callback=self.on_otp_callback
on_otp_callback=self.on_otp_callback,
reconnect_policy=getattr(self, "reconnect_policy", None),
)

self.api.trace_ws = self.debug_ws_enable
Expand Down
36 changes: 34 additions & 2 deletions pyquotex/_api/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,21 @@
from pyquotex import expiration
from pyquotex._api._constants import DEFAULT_TIMEOUT, _request_counter
from pyquotex.utils import json_utils as json
from pyquotex.utils.cache import TTLCache
from pyquotex.utils.processor import (
calculate_candles,
process_candles_v2,
merge_candles,
)

# Per-process cache of recent get_candles() responses. Keyed by
# (asset, period, offset, candle_bucket). TTL stays well under the candle
# period so live data is never served stale. Off by default; opt in by
# passing use_cache=True.
_CANDLE_CACHE: TTLCache[tuple[str, int, int, int], list[dict[str, Any]]] = (
TTLCache(maxsize=128, ttl=10.0)
)

logger = logging.getLogger(__name__)


Expand All @@ -33,15 +42,33 @@ async def get_candles(
offset: int,
period: int,
progressive: bool = False,
timeout: int = DEFAULT_TIMEOUT
timeout: int = DEFAULT_TIMEOUT,
use_cache: bool = False,
) -> list[dict[str, Any]] | None:
"""Retrieves candles for a specific asset."""
"""Retrieve candles for a specific asset.

Parameters
----------
use_cache:
When ``True``, identical ``(asset, period, offset, candle_bucket)``
requests within the cache TTL (~10s) are served from memory.
``candle_bucket`` floors ``end_from_time`` to the candle period
so live data is never served stale.
"""
if self.api is None:
return None

if end_from_time is None:
end_from_time = time.time()

cache_key: tuple[str, int, int, int] | None = None
if use_cache and not progressive and period > 0:
bucket = int(end_from_time // period)
cache_key = (asset, period, offset, bucket)
cached = _CANDLE_CACHE.get(cache_key)
if cached is not None:
return cached

index = expiration.get_timestamp()
self.api.candles.candles_data = None

Expand Down Expand Up @@ -71,6 +98,11 @@ async def get_candles(
if progressive:
return self.api.historical_candles.get("data", {})

if cache_key is not None and candles:
# TTL is the minimum between period and the cache default so
# the candle bucket never serves data after the candle closes.
_CANDLE_CACHE.set(cache_key, candles, ttl=min(_CANDLE_CACHE.ttl, period))

return candles

async def _fetch_historical_batch(
Expand Down
4 changes: 4 additions & 0 deletions pyquotex/_api/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,14 @@ async def start_candles_stream(
await self.api.subscribe_realtime_candle(asset, period)
await self.api.chart_notification(asset)
await self.api.follow_candle(asset)
self.api._track_subscription("candle", asset, period)

async def stop_candles_stream(self, asset: str) -> None:
"""Stops streaming candle data for a specified asset."""
if self.api:
await self.api.unsubscribe_realtime_candle(asset)
await self.api.unfollow_candle(asset)
self.api._forget_subscription("candle", asset)

async def start_signals_data(self) -> None:
"""Subscribes to the global trading signals stream."""
Expand Down Expand Up @@ -555,6 +557,7 @@ async def start_candles_all_size_stream(self, asset: str) -> bool:
self.api.candle_generated_all_size_check[str(asset)] = {}
if not (str(asset) in self.subscribe_candle_all_size):
self.subscribe_candle_all_size.append(str(asset))
self.api._track_subscription("candle_all_size", asset)
start = time.time()
while await self.check_connect():
if self.api is None: break
Expand Down Expand Up @@ -590,6 +593,7 @@ async def start_mood_stream(

if asset not in self.subscribe_mood:
self.subscribe_mood.append(asset)
self.api._track_subscription("mood", asset, instrument=instrument)
while True:
if self.api is None: break
if hasattr(self.api, "subscribe_Traders_mood"):
Expand Down
25 changes: 13 additions & 12 deletions pyquotex/_api/trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from pyquotex import expiration
from pyquotex._api._constants import DEFAULT_TIMEOUT
from pyquotex._api._waits import wait_until
from pyquotex.utils.account_type import AccountType

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -137,18 +138,18 @@ async def sell_option(
# arrives before the next line, it must not be wiped out.
self.api.sold_options_respond = None
await self.api.sell_option(options_ids)
# TODO(refactor/architecture Phase 2.5): polling here cannot be migrated
# to SlotRegistry until we identify the WS event that should populate
# self.api.sold_options_respond. No producer exists in
# pyquotex/api.py:_on_message, so this method currently only exits via
# the timeout branch below (or never, if no explicit timeout). Same
# situation as edit_practice_balance — investigate WS traffic during
# sell_option calls to find the correct event.
start = time.time()
while self.api.sold_options_respond is None:
if time.time() - start > timeout:
raise TimeoutError("Timeout waiting for sell option response.")
await asyncio.sleep(0.2)
# We don't yet have an identified WS event that populates
# sold_options_respond, so we still settle via predicate-polling —
# but through wait_until() (event-loop friendly, hard timeout,
# configurable poll interval) instead of an ad-hoc while/sleep.
try:
await wait_until(
lambda: self.api.sold_options_respond is not None,
timeout=float(timeout),
poll_interval=0.2,
)
except asyncio.TimeoutError:
raise TimeoutError("Timeout waiting for sell option response.")
return self.api.sold_options_respond

async def check_win(
Expand Down
Loading
Loading