diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..ac2e380 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,48 @@ +name: Docs + +on: + push: + branches: [main] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build-docs: + name: Build documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - run: uv sync --frozen --extra docs + + - run: uv run sphinx-build -b html docs/ docs/_build/html + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/_build/html + + deploy: + name: Deploy to GitHub Pages + needs: build-docs + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index e7345ae..d560b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ coverage.xml .mypy_cache/ .ruff_cache/ +# Sphinx documentation +docs/_build/ + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..08835bd --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api/client.rst b/docs/api/client.rst new file mode 100644 index 0000000..827cbb5 --- /dev/null +++ b/docs/api/client.rst @@ -0,0 +1,32 @@ +Client +====== + +The main entry point for the SDK. :class:`~etoropy.EToroTrading` wraps REST +endpoints, WebSocket streaming, and instrument resolution behind a single +async interface. + +EToroTrading +------------ + +.. autoclass:: etoropy.EToroTrading + :members: + :show-inheritance: + +OrderOptions +------------ + +.. autoclass:: etoropy.OrderOptions + :members: + +InstrumentResolver +------------------ + +.. autoclass:: etoropy.InstrumentResolver + :members: + :show-inheritance: + +InstrumentInfo +-------------- + +.. autoclass:: etoropy.InstrumentInfo + :members: diff --git a/docs/api/config.rst b/docs/api/config.rst new file mode 100644 index 0000000..c02b176 --- /dev/null +++ b/docs/api/config.rst @@ -0,0 +1,6 @@ +Configuration +============= + +.. autopydantic_settings:: etoropy.EToroConfig + :members: + :show-inheritance: diff --git a/docs/api/errors.rst b/docs/api/errors.rst new file mode 100644 index 0000000..df07d1b --- /dev/null +++ b/docs/api/errors.rst @@ -0,0 +1,55 @@ +Errors +====== + +All SDK errors inherit from :class:`~etoropy.EToroError`: + +.. code-block:: text + + EToroError + +-- EToroApiError # HTTP 4xx/5xx + | +-- EToroRateLimitError # HTTP 429 + +-- EToroAuthError # HTTP 401/403 or WS auth failure + +-- EToroValidationError # Invalid input + +-- EToroWebSocketError # WS connection/protocol errors + +EToroError +---------- + +.. autoclass:: etoropy.EToroError + :members: + :show-inheritance: + +EToroApiError +------------- + +.. autoclass:: etoropy.EToroApiError + :members: + :show-inheritance: + +EToroRateLimitError +------------------- + +.. autoclass:: etoropy.EToroRateLimitError + :members: + :show-inheritance: + +EToroAuthError +-------------- + +.. autoclass:: etoropy.EToroAuthError + :members: + :show-inheritance: + +EToroValidationError +-------------------- + +.. autoclass:: etoropy.EToroValidationError + :members: + :show-inheritance: + +EToroWebSocketError +------------------- + +.. autoclass:: etoropy.EToroWebSocketError + :members: + :show-inheritance: diff --git a/docs/api/http.rst b/docs/api/http.rst new file mode 100644 index 0000000..b311f5d --- /dev/null +++ b/docs/api/http.rst @@ -0,0 +1,36 @@ +HTTP Layer +========== + +Low-level HTTP transport with rate limiting and retry logic. + +HttpClient +---------- + +.. autoclass:: etoropy.HttpClient + :members: + :show-inheritance: + +RequestOptions +-------------- + +.. autoclass:: etoropy.RequestOptions + :members: + +RateLimiter +----------- + +.. autoclass:: etoropy.RateLimiter + :members: + :show-inheritance: + +RateLimiterOptions +------------------ + +.. autoclass:: etoropy.RateLimiterOptions + :members: + +RetryOptions +------------ + +.. autoclass:: etoropy.http.RetryOptions + :members: diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..73201b1 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,13 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 2 + + client + config + models + rest + websocket + http + errors diff --git a/docs/api/models.rst b/docs/api/models.rst new file mode 100644 index 0000000..1cabfcb --- /dev/null +++ b/docs/api/models.rst @@ -0,0 +1,47 @@ +Models +====== + +All data models are `Pydantic v2 `_ +``BaseModel`` subclasses with full type annotations. + +Enums +----- + +.. automodule:: etoropy.models.enums + :members: + :show-inheritance: + +Common +------ + +.. automodule:: etoropy.models.common + :members: + :show-inheritance: + +Market Data +----------- + +.. automodule:: etoropy.models.market_data + :members: + :show-inheritance: + +Trading +------- + +.. automodule:: etoropy.models.trading + :members: + :show-inheritance: + +Feeds & Social +-------------- + +.. automodule:: etoropy.models.feeds + :members: + :show-inheritance: + +WebSocket Messages +------------------ + +.. automodule:: etoropy.models.websocket + :members: + :show-inheritance: diff --git a/docs/api/rest.rst b/docs/api/rest.rst new file mode 100644 index 0000000..0fef651 --- /dev/null +++ b/docs/api/rest.rst @@ -0,0 +1,75 @@ +REST Clients +============ + +The :class:`~etoropy.RestClient` facade composes nine specialized sub-clients, +one per API domain. + +RestClient +---------- + +.. autoclass:: etoropy.RestClient + :members: + :show-inheritance: + +MarketDataClient +---------------- + +.. autoclass:: etoropy.MarketDataClient + :members: + :show-inheritance: + +TradingExecutionClient +---------------------- + +.. autoclass:: etoropy.TradingExecutionClient + :members: + :show-inheritance: + +TradingInfoClient +----------------- + +.. autoclass:: etoropy.TradingInfoClient + :members: + :show-inheritance: + +WatchlistsClient +---------------- + +.. autoclass:: etoropy.WatchlistsClient + :members: + :show-inheritance: + +FeedsClient +----------- + +.. autoclass:: etoropy.FeedsClient + :members: + :show-inheritance: + +ReactionsClient +--------------- + +.. autoclass:: etoropy.ReactionsClient + :members: + :show-inheritance: + +DiscoveryClient +--------------- + +.. autoclass:: etoropy.DiscoveryClient + :members: + :show-inheritance: + +UsersInfoClient +--------------- + +.. autoclass:: etoropy.UsersInfoClient + :members: + :show-inheritance: + +PiDataClient +------------- + +.. autoclass:: etoropy.PiDataClient + :members: + :show-inheritance: diff --git a/docs/api/websocket.rst b/docs/api/websocket.rst new file mode 100644 index 0000000..d2dc4c7 --- /dev/null +++ b/docs/api/websocket.rst @@ -0,0 +1,31 @@ +WebSocket +========= + +Real-time streaming via the eToro WebSocket API. + +WsClient +-------- + +.. autoclass:: etoropy.WsClient + :members: + :show-inheritance: + +WsClientOptions +--------------- + +.. autoclass:: etoropy.WsClientOptions + :members: + +Message Parser +-------------- + +.. automodule:: etoropy.ws.message_parser + :members: + :show-inheritance: + +Subscription Tracker +-------------------- + +.. autoclass:: etoropy.ws.WsSubscriptionTracker + :members: + :show-inheritance: diff --git a/docs/architecture.rst b/docs/architecture.rst new file mode 100644 index 0000000..b43f741 --- /dev/null +++ b/docs/architecture.rst @@ -0,0 +1,179 @@ +Architecture +============ + +etoropy is organized in layers. The high-level +:class:`~etoropy.EToroTrading` client composes REST, WebSocket, and +instrument-resolution sub-systems and exposes a unified async API. + +Layer diagram +------------- + +.. code-block:: text + + +------------------------------------------------------+ + | EToroTrading (trading/client.py) | + | High-level: buy, sell, stream, wait_for_order | + +----+-----------------------------+-------------------+ + | | + +----v-------------+ +-----------v-----------+ + | RestClient | | WsClient | + | (9 sub-clients) | | auth, heartbeat, | + | | | reconnect, events | + +----+-------------+ +-----------+-----------+ + | | + +----v-------------+ +-----------v-----------+ + | HttpClient | | websockets lib | + | httpx + retry | | (ping_interval for | + | + rate limiter | | heartbeat) | + +------------------+ +-----------------------+ + +Package layout +-------------- + +.. code-block:: text + + etoropy/ + __init__.py # Public API exports + _utils.py # UUID generation + config/ + settings.py # EToroConfig (pydantic-settings) + constants.py # URLs, defaults, limits + errors/ + exceptions.py # 6-class error hierarchy + models/ + enums.py # CandleInterval, OrderStatusId, etc. + common.py # Pagination, TokenResponse + market_data.py # Instrument, Rate, Candle models + trading.py # Order, Position, Portfolio models + feeds.py # Social feed, user profile models + websocket.py # WsEnvelope, WsInstrumentRate, WsPrivateEvent + http/ + client.py # HttpClient (httpx wrapper with auth, retry, rate limiting) + rate_limiter.py # Token-bucket rate limiter + retry.py # Exponential backoff with jitter + rest/ + _base.py # BaseRestClient (GET/POST/PUT/DELETE helpers) + rest_client.py # RestClient facade (composes all sub-clients) + market_data.py # 8 endpoints + trading_execution.py # 7 endpoints (demo/real routing) + trading_info.py # 4 endpoints (demo/real routing) + watchlists.py # 14 endpoints + feeds.py # 3 endpoints + reactions.py # 1 endpoint + discovery.py # 2 endpoints + pi_data.py # 1 endpoint + users_info.py # 6 endpoints + ws/ + client.py # WsClient (auth, heartbeat, reconnect, events) + message_parser.py # Parse WS envelopes into typed events + subscription.py # Topic set tracking for reconnect re-subscribe + trading/ + client.py # EToroTrading (high-level entry point) + instrument_resolver.py # Symbol <-> ID resolution (CSV + API) + data/ + instruments.csv # 5,200+ symbol mappings + +REST sub-clients +---------------- + +The :class:`~etoropy.RestClient` facade composes nine specialized clients: + +.. list-table:: + :header-rows: 1 + :widths: 30 10 60 + + * - Client + - Endpoints + - Description + * - :class:`~etoropy.MarketDataClient` + - 8 + - Instruments, rates, candles, exchanges, industries + * - :class:`~etoropy.TradingExecutionClient` + - 7 + - Market/limit orders, close positions, cancel orders + * - :class:`~etoropy.TradingInfoClient` + - 4 + - Portfolio, P&L, order status, trade history + * - :class:`~etoropy.WatchlistsClient` + - 14 + - CRUD for user/public watchlists + * - :class:`~etoropy.FeedsClient` + - 3 + - Instrument/user feeds, post creation + * - :class:`~etoropy.ReactionsClient` + - 1 + - Comment creation + * - :class:`~etoropy.DiscoveryClient` + - 2 + - Curated lists, market recommendations + * - :class:`~etoropy.UsersInfoClient` + - 6 + - User profiles, portfolios, performance, search + * - :class:`~etoropy.PiDataClient` + - 1 + - Copier public info + +WebSocket event system +---------------------- + +:class:`~etoropy.EToroTrading` exposes a Node.js-style event emitter: + +.. list-table:: + :header-rows: 1 + :widths: 20 35 45 + + * - Event + - Callback signature + - Description + * - ``"price"`` + - ``(symbol, instrument_id, WsInstrumentRate)`` + - Live price tick + * - ``"order:update"`` + - ``(WsPrivateEvent)`` + - Order status change + * - ``"connected"`` + - ``()`` + - WebSocket connected and authenticated + * - ``"disconnected"`` + - ``()`` + - Client disconnected + * - ``"error"`` + - ``(Exception)`` + - Any error + * - ``"ws:message"`` + - ``(WsEnvelope)`` + - Raw WebSocket envelope + +Register handlers with :meth:`~etoropy.EToroTrading.on`, +:meth:`~etoropy.EToroTrading.off`, and +:meth:`~etoropy.EToroTrading.once`. + +Instrument resolution +--------------------- + +The :class:`~etoropy.InstrumentResolver` translates human-readable symbols +(``"AAPL"``, ``"BTC"``) into eToro's integer instrument IDs through three +tiers: + +1. **Bundled CSV** -- 5,200+ pre-mapped symbols loaded with + ``load_bundled_csv()``. Instant, no network call. +2. **API exact match** -- queries ``/market-data/search`` by + ``internalSymbolFull``. +3. **API text search** -- fallback free-text search on the same endpoint. + +Results are cached in memory for the lifetime of the client. + +Rate limiting & retry +--------------------- + +**Rate limiter**: Token-bucket algorithm (20 requests per 10-second window by +default). Automatically pauses outgoing requests when the bucket is full. +Honors ``Retry-After`` headers from 429 responses. + +**Retry**: Exponential backoff with jitter (+-25%). Retries on: + +- HTTP 429 (rate limit) +- HTTP 5xx (server error) +- Connection errors and read timeouts + +Default: 3 attempts, 1-second base delay, 2x backoff multiplier. diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..e2a2d56 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,23 @@ +Changelog +========= + +v0.1.1 (2026-02-26) +-------------------- + +- Add Sphinx documentation site with Furo theme and autodoc API reference +- Add quickstart, architecture, and examples guides +- Add GitHub Actions workflow for automatic docs deploy to GitHub Pages + +v0.1.0 (2025) +-------------- + +Initial release. + +- 42+ REST endpoints across 9 sub-clients (market data, trading execution, + trading info, watchlists, feeds, reactions, discovery, user info, PI data) +- Real-time WebSocket streaming with auto-reconnect and heartbeat +- :class:`~etoropy.EToroTrading` high-level client with event emitter +- :class:`~etoropy.InstrumentResolver` with bundled 5,200+ symbol CSV +- Token-bucket rate limiter and exponential-backoff retry +- Full type hints (mypy strict) and Pydantic v2 models +- Demo and real trading mode support diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..8f2f5cb --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,75 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import importlib.metadata + +# -- Project information ----------------------------------------------------- + +project = "etoropy" +copyright = "2025, Massimo Gollo" +author = "Massimo Gollo" +release = importlib.metadata.version("etoropy") +version = ".".join(release.split(".")[:2]) + +# -- General configuration --------------------------------------------------- + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx_autodoc_typehints", + "sphinx_copybutton", + "sphinxcontrib.autodoc_pydantic", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# -- Options for HTML output ------------------------------------------------- + +html_theme = "furo" +html_title = "etoropy" +html_static_path = [] + +# -- autodoc ----------------------------------------------------------------- + +autodoc_default_options = { + "members": True, + "show-inheritance": True, + "undoc-members": False, +} +autodoc_member_order = "bysource" +autodoc_typehints = "description" + +# -- sphinx-autodoc-typehints ------------------------------------------------ + +always_document_param_types = True +typehints_defaults = "braces" + +# -- autodoc-pydantic -------------------------------------------------------- + +autodoc_pydantic_model_show_json = False +autodoc_pydantic_model_show_config_summary = False +autodoc_pydantic_model_show_field_summary = True +autodoc_pydantic_model_show_validator_summary = True +autodoc_pydantic_field_list_validators = True +autodoc_pydantic_settings_show_json = False +autodoc_pydantic_settings_show_config_summary = False +autodoc_pydantic_settings_show_field_summary = True + +# -- intersphinx ------------------------------------------------------------- + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "pydantic": ("https://docs.pydantic.dev/latest/", None), +} + +suppress_warnings = ["sphinx_autodoc_typehints.forward_reference"] + +# -- copybutton -------------------------------------------------------------- + +copybutton_prompt_text = r">>> |\.\.\. |\$ " +copybutton_prompt_is_regexp = True diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..4df95aa --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,126 @@ +Examples +======== + +The examples below are taken from the ``examples/`` directory in the +repository. Each is a standalone script that you can run with: + +.. code-block:: bash + + uv run python examples/