Skip to content

Commit 38ee48f

Browse files
pimfeltkampclaude
andcommitted
Add 4 resources (signals, arbitrage, marketmaker, template) — v0.2.0a1
Ports the A1 roadmap wave from @cryptohopper/sdk@0.2.0-alpha.1 to Python. Same 4 domains, same endpoint mapping, same hyphenated-path handling (market-cancel, get-backlogs, delete-backlog, save-template, set-market-trend). Method names are snake_case per Python idiom (chart_data, exchange_start, get_market_trend, etc). All 40 tests passing (11 new), ruff clean, mypy --strict clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ce601c6 commit 38ee48f

9 files changed

Lines changed: 419 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@
33
All notable changes to the `cryptohopper` Python package are documented in this file.
44
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

6-
## 0.1.0a1 — Unreleased
6+
## 0.2.0a1 — Unreleased
7+
8+
Adds four more API domains: `signals`, `arbitrage`, `marketmaker`, `template`.
9+
10+
### Added
11+
- **`signals`**`list`, `performance`, `stats`, `distribution`, `chart_data`.
12+
- **`arbitrage`**`exchange_start`, `exchange_cancel`, `exchange_results`, `exchange_history`, `exchange_total`, `exchange_reset_total`, `market_start`, `market_cancel`, `market_result`, `market_history`, `backlogs`, `backlog`, `delete_backlog`.
13+
- **`marketmaker`**`get`, `cancel`, `history`, `get_market_trend`, `set_market_trend`, `delete_market_trend`, `backlogs`, `backlog`, `delete_backlog`.
14+
- **`template`**`list`, `get`, `basic`, `save`, `update`, `load`, `delete`.
15+
16+
## 0.1.0a1 — 2026-04-24
717

818
Initial release. Covers six core API domains.
919

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "cryptohopper"
7-
version = "0.1.0a1"
7+
version = "0.2.0a1"
88
description = "Official Python SDK for the Cryptohopper API"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/cryptohopper/_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@
2424
HttpMethod = Literal["GET", "POST", "PATCH", "DELETE", "PUT"]
2525

2626
if TYPE_CHECKING:
27+
from .resources.arbitrage import Arbitrage
2728
from .resources.backtest import Backtests
2829
from .resources.exchange import Exchange
2930
from .resources.hoppers import Hoppers
3031
from .resources.market import Market
32+
from .resources.marketmaker import MarketMaker
33+
from .resources.signals import Signals
3134
from .resources.strategy import Strategies
35+
from .resources.template import Templates
3236
from .resources.user import User
3337

3438

@@ -62,6 +66,10 @@ class CryptohopperClient:
6266
backtest: Backtests
6367
market: Market
6468
user: User
69+
signals: Signals
70+
arbitrage: Arbitrage
71+
marketmaker: MarketMaker
72+
template: Templates
6573

6674
def __init__(
6775
self,
@@ -86,11 +94,15 @@ def __init__(
8694
self._owns_http = http_client is None
8795

8896
# Import here to avoid a circular at module import time.
97+
from .resources.arbitrage import Arbitrage
8998
from .resources.backtest import Backtests
9099
from .resources.exchange import Exchange
91100
from .resources.hoppers import Hoppers
92101
from .resources.market import Market
102+
from .resources.marketmaker import MarketMaker
103+
from .resources.signals import Signals
93104
from .resources.strategy import Strategies
105+
from .resources.template import Templates
94106
from .resources.user import User
95107

96108
self.user = User(self)
@@ -99,6 +111,10 @@ def __init__(
99111
self.strategy = Strategies(self)
100112
self.backtest = Backtests(self)
101113
self.market = Market(self)
114+
self.signals = Signals(self)
115+
self.arbitrage = Arbitrage(self)
116+
self.marketmaker = MarketMaker(self)
117+
self.template = Templates(self)
102118

103119
def __enter__(self) -> CryptohopperClient:
104120
return self

src/cryptohopper/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
CURRENT_VERSION = "0.1.0a1"
1+
CURRENT_VERSION = "0.2.0a1"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""``client.arbitrage`` — exchange + market arbitrage operations.
2+
3+
Two distinct flavours:
4+
• ``exchange_*`` — cross-exchange arbitrage.
5+
• ``market_*`` — intra-exchange market arbitrage.
6+
Plus a shared backlog API.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from collections.abc import Sequence
12+
from typing import TYPE_CHECKING, Any
13+
14+
if TYPE_CHECKING:
15+
from .._client import CryptohopperClient
16+
17+
BacklogId = int | str
18+
19+
20+
class Arbitrage:
21+
def __init__(self, client: CryptohopperClient) -> None:
22+
self._client = client
23+
24+
# ─── Cross-exchange arbitrage ─────────────────────────────────────────
25+
26+
def exchange_start(self, data: dict[str, Any]) -> dict[str, Any]:
27+
"""Start a cross-exchange arbitrage run. Requires ``trade``."""
28+
return self._client._request("POST", "/arbitrage/exchange", json=data)
29+
30+
def exchange_cancel(self, data: dict[str, Any] | None = None) -> dict[str, Any]:
31+
"""Cancel a cross-exchange arbitrage run. Requires ``trade``."""
32+
return self._client._request("POST", "/arbitrage/cancel", json=data or {})
33+
34+
def exchange_results(self, **params: Any) -> Sequence[dict[str, Any]]:
35+
"""Fetch results of exchange-arbitrage runs. Requires ``read``."""
36+
return self._client._request(
37+
"GET", "/arbitrage/results", params=params or None
38+
)
39+
40+
def exchange_history(self, **params: Any) -> Sequence[dict[str, Any]]:
41+
"""Historical exchange-arbitrage runs. Requires ``read``."""
42+
return self._client._request(
43+
"GET", "/arbitrage/history", params=params or None
44+
)
45+
46+
def exchange_total(self) -> dict[str, Any]:
47+
"""Running totals across exchange-arbitrage runs. Requires ``read``."""
48+
return self._client._request("GET", "/arbitrage/total")
49+
50+
def exchange_reset_total(self) -> dict[str, Any]:
51+
"""Reset the running totals. Requires ``manage``."""
52+
return self._client._request("POST", "/arbitrage/resettotal", json={})
53+
54+
# ─── Intra-exchange market arbitrage ──────────────────────────────────
55+
56+
def market_start(self, data: dict[str, Any]) -> dict[str, Any]:
57+
"""Start an intra-exchange arbitrage run. Requires ``trade``."""
58+
return self._client._request("POST", "/arbitrage/market", json=data)
59+
60+
def market_cancel(self, data: dict[str, Any] | None = None) -> dict[str, Any]:
61+
"""Cancel a market-arbitrage run. Requires ``trade``."""
62+
return self._client._request("POST", "/arbitrage/market-cancel", json=data or {})
63+
64+
def market_result(self, **params: Any) -> dict[str, Any]:
65+
"""Result of a specific market-arbitrage run. Requires ``read``."""
66+
return self._client._request(
67+
"GET", "/arbitrage/market-result", params=params or None
68+
)
69+
70+
def market_history(self, **params: Any) -> Sequence[dict[str, Any]]:
71+
"""Historical market-arb runs. Requires ``read``."""
72+
return self._client._request(
73+
"GET", "/arbitrage/market-history", params=params or None
74+
)
75+
76+
# ─── Backlog (shared) ────────────────────────────────────────────────
77+
78+
def backlogs(self, **params: Any) -> Sequence[dict[str, Any]]:
79+
"""List queued/pending backlog items. Requires ``read``."""
80+
return self._client._request(
81+
"GET", "/arbitrage/get-backlogs", params=params or None
82+
)
83+
84+
def backlog(self, backlog_id: BacklogId) -> dict[str, Any]:
85+
"""Fetch a single backlog item. Requires ``read``."""
86+
return self._client._request(
87+
"GET", "/arbitrage/get-backlog", params={"backlog_id": backlog_id}
88+
)
89+
90+
def delete_backlog(self, backlog_id: BacklogId) -> dict[str, Any]:
91+
"""Delete a backlog item. Requires ``manage``."""
92+
return self._client._request(
93+
"POST", "/arbitrage/delete-backlog", json={"backlog_id": backlog_id}
94+
)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""``client.marketmaker`` — market-maker bot ops + market-trend overrides + backlog."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
from typing import TYPE_CHECKING, Any
7+
8+
if TYPE_CHECKING:
9+
from .._client import CryptohopperClient
10+
11+
BacklogId = int | str
12+
13+
14+
class MarketMaker:
15+
def __init__(self, client: CryptohopperClient) -> None:
16+
self._client = client
17+
18+
def get(self, **params: Any) -> dict[str, Any]:
19+
"""Fetch the market-maker state for a hopper. Requires ``read``."""
20+
return self._client._request("GET", "/marketmaker/get", params=params or None)
21+
22+
def cancel(self, data: dict[str, Any] | None = None) -> dict[str, Any]:
23+
"""Cancel running market-maker orders. Requires ``trade``."""
24+
return self._client._request("POST", "/marketmaker/cancel", json=data or {})
25+
26+
def history(self, **params: Any) -> Sequence[dict[str, Any]]:
27+
"""Historical order activity. Requires ``read``."""
28+
return self._client._request(
29+
"GET", "/marketmaker/history", params=params or None
30+
)
31+
32+
# ─── Market-trend overrides ──────────────────────────────────────────
33+
34+
def get_market_trend(self, **params: Any) -> dict[str, Any]:
35+
"""Read the current market-trend override. Requires ``read``."""
36+
return self._client._request(
37+
"GET", "/marketmaker/get-market-trend", params=params or None
38+
)
39+
40+
def set_market_trend(self, data: dict[str, Any]) -> dict[str, Any]:
41+
"""Set a market-trend override. Requires ``manage``."""
42+
return self._client._request(
43+
"POST", "/marketmaker/set-market-trend", json=data
44+
)
45+
46+
def delete_market_trend(
47+
self, data: dict[str, Any] | None = None
48+
) -> dict[str, Any]:
49+
"""Remove the current market-trend override. Requires ``manage``."""
50+
return self._client._request(
51+
"POST", "/marketmaker/delete-market-trend", json=data or {}
52+
)
53+
54+
# ─── Backlog ─────────────────────────────────────────────────────────
55+
56+
def backlogs(self, **params: Any) -> Sequence[dict[str, Any]]:
57+
"""List queued/pending market-maker backlog items. Requires ``read``."""
58+
return self._client._request(
59+
"GET", "/marketmaker/get-backlogs", params=params or None
60+
)
61+
62+
def backlog(self, backlog_id: BacklogId) -> dict[str, Any]:
63+
"""Fetch a single backlog item. Requires ``read``."""
64+
return self._client._request(
65+
"GET", "/marketmaker/get-backlog", params={"backlog_id": backlog_id}
66+
)
67+
68+
def delete_backlog(self, backlog_id: BacklogId) -> dict[str, Any]:
69+
"""Delete a backlog item. Requires ``manage``."""
70+
return self._client._request(
71+
"POST", "/marketmaker/delete-backlog", json={"backlog_id": backlog_id}
72+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""``client.signals`` — signal-provider analytics.
2+
3+
Distinct from ``client.market.signals``, which browses marketplace signals
4+
as a consumer.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from collections.abc import Sequence
10+
from typing import TYPE_CHECKING, Any
11+
12+
if TYPE_CHECKING:
13+
from .._client import CryptohopperClient
14+
15+
16+
class Signals:
17+
def __init__(self, client: CryptohopperClient) -> None:
18+
self._client = client
19+
20+
def list(self, **params: Any) -> Sequence[dict[str, Any]]:
21+
"""Signals this provider has published."""
22+
return self._client._request("GET", "/signals/signals", params=params or None)
23+
24+
def performance(self, **params: Any) -> dict[str, Any]:
25+
"""Performance stats (winrate, avg profit per signal, etc.)."""
26+
return self._client._request(
27+
"GET", "/signals/performance", params=params or None
28+
)
29+
30+
def stats(self) -> dict[str, Any]:
31+
"""Overall provider stats."""
32+
return self._client._request("GET", "/signals/stats")
33+
34+
def distribution(self) -> dict[str, Any]:
35+
"""Distribution of signals across exchanges / markets / types."""
36+
return self._client._request("GET", "/signals/distribution")
37+
38+
def chart_data(self, **params: Any) -> dict[str, Any]:
39+
"""Data series for charting the provider's performance over time."""
40+
return self._client._request("GET", "/signals/chartdata", params=params or None)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""``client.template`` — bot templates (reusable hopper configurations)."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
from typing import TYPE_CHECKING, Any
7+
8+
if TYPE_CHECKING:
9+
from .._client import CryptohopperClient
10+
11+
TemplateId = int | str
12+
13+
14+
class Templates:
15+
def __init__(self, client: CryptohopperClient) -> None:
16+
self._client = client
17+
18+
def list(self) -> Sequence[dict[str, Any]]:
19+
"""List all templates the user has access to. Requires ``read``."""
20+
return self._client._request("GET", "/template/templates")
21+
22+
def get(self, template_id: TemplateId) -> dict[str, Any]:
23+
"""Fetch a template. Requires ``read``."""
24+
return self._client._request(
25+
"GET", "/template/get", params={"template_id": template_id}
26+
)
27+
28+
def basic(self, template_id: TemplateId) -> dict[str, Any]:
29+
"""Fetch the basic (lightweight) view of a template. Requires ``read``."""
30+
return self._client._request(
31+
"GET", "/template/basic", params={"template_id": template_id}
32+
)
33+
34+
def save(self, data: dict[str, Any]) -> dict[str, Any]:
35+
"""Save a new template. Requires ``manage``."""
36+
return self._client._request("POST", "/template/save-template", json=data)
37+
38+
def update(
39+
self, template_id: TemplateId, data: dict[str, Any]
40+
) -> dict[str, Any]:
41+
"""Update an existing template. Requires ``manage``."""
42+
return self._client._request(
43+
"POST", "/template/update", json={"template_id": template_id, **data}
44+
)
45+
46+
def load(
47+
self, template_id: TemplateId, hopper_id: TemplateId
48+
) -> dict[str, Any]:
49+
"""Apply a template to a hopper. Requires ``manage``."""
50+
return self._client._request(
51+
"POST",
52+
"/template/load",
53+
json={"template_id": template_id, "hopper_id": hopper_id},
54+
)
55+
56+
def delete(self, template_id: TemplateId) -> dict[str, Any]:
57+
"""Delete a template. Requires ``manage``."""
58+
return self._client._request(
59+
"POST", "/template/delete", json={"template_id": template_id}
60+
)

0 commit comments

Comments
 (0)