Skip to content

Commit 0b89575

Browse files
authored
Merge pull request #2 from perps-studio/claude/recursing-goodall-4c5a39
feat: initial port from ts
2 parents d45ed1d + 14dc462 commit 0b89575

50 files changed

Lines changed: 2302 additions & 89 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ from hip4.types import PredictionOrderParams
4848
adapter = create_hip4_adapter()
4949
adapter.initialize()
5050

51-
# Agent key (separate from the user's wallet sign with the user's wallet
51+
# Agent key (separate from the user's wallet - sign with the user's wallet
5252
# once via auth_eoa.py to approve, then store the agent key for trading).
5353
agent = LocalSigner(os.environ["HIP4_AGENT_PRIVATE_KEY"])
5454
adapter.auth.init_auth(os.environ["HIP4_USER_ADDRESS"], agent)
@@ -222,4 +222,4 @@ Signing implementation inspired by [`@nktkas/hyperliquid`](https://github.com/nk
222222

223223
## License
224224

225-
BUSL-1.1 same as the TypeScript SDK.
225+
BUSL-1.1 - same as the TypeScript SDK.

examples/async_get_all_markets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Async equivalent of ``get_all_markets.py`` requires ``hip4[async]``."""
1+
"""Async equivalent of ``get_all_markets.py`` - requires ``hip4[async]``."""
22

33
from __future__ import annotations
44

examples/auth_eoa.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ def main() -> None:
3232
adapter = create_hip4_adapter()
3333
adapter.client.testnet = not is_mainnet # already true by default; explicit here for clarity.
3434

35-
# 2. User's wallet (in real life this is the connected wallet Metamask, etc.)
35+
# 2. User's wallet (in real life this is the connected wallet - Metamask, etc.)
3636
user_pk = os.environ.get("HIP4_USER_PRIVATE_KEY")
3737
if not user_pk:
3838
raise SystemExit("Set HIP4_USER_PRIVATE_KEY to a hex private key for this demo.")
3939
user_signer = LocalSigner(user_pk)
4040
user_address = user_signer.get_address()
4141

42-
# 3. Agent key a fresh, scoped signer used for order signing only.
42+
# 3. Agent key - a fresh, scoped signer used for order signing only.
4343
agent_signer = LocalSigner.create()
4444
agent_address = agent_signer.get_address()
4545
agent_name = "hip4-py-demo"
@@ -63,7 +63,7 @@ def main() -> None:
6363
)
6464
print("approve_agent:", result)
6565

66-
# 6. Wire the agent into the adapter orders are now signed by the agent
66+
# 6. Wire the agent into the adapter - orders are now signed by the agent
6767
# on behalf of the user's address.
6868
adapter.auth.init_auth(user_address, agent_signer)
6969
print("auth status:", adapter.auth.get_auth_status())

hip4/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""hip4 Python SDK for Hyperliquid HIP-4 prediction markets.
1+
"""hip4 - Python SDK for Hyperliquid HIP-4 prediction markets.
22
33
Quick start (synchronous)::
44

hip4/_events_core.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import replace
6-
from typing import Dict, List, Mapping, Optional, Tuple
6+
from typing import Any, Dict, List, Mapping, Optional, Tuple
77

88
from hip4.coin import side_coin
99
from hip4.types.event import (
@@ -16,6 +16,7 @@
1616

1717
__all__ = [
1818
"CATEGORIES",
19+
"apply_outcome_created_to_side_names",
1920
"build_events_from_meta",
2021
"extract_side_names",
2122
"merge_mids_into_events",
@@ -156,6 +157,24 @@ def build_events_from_meta(meta: HLOutcomeMeta) -> List[PredictionEvent]:
156157
return events
157158

158159

160+
def apply_outcome_created_to_side_names(
161+
side_names: Dict[int, Tuple[str, str]],
162+
spec: Mapping[str, Any],
163+
) -> None:
164+
"""Mutate *side_names* to include a newly-created outcome from a WS update.
165+
166+
No-op if the spec doesn't carry both side names (under-specified outcome).
167+
Mirrors the TS adapter's behaviour: extend in place so callers using
168+
:meth:`get_side_name_resolver` pick up the new outcome immediately.
169+
"""
170+
side_specs = spec.get("sideSpecs") or []
171+
if len(side_specs) >= 2:
172+
side_names[spec["outcome"]] = (
173+
side_specs[0].get("name", "Side 0"),
174+
side_specs[1].get("name", "Side 1"),
175+
)
176+
177+
159178
def extract_side_names(meta: HLOutcomeMeta) -> Dict[int, Tuple[str, str]]:
160179
"""Build the ``outcome_id -> (side0, side1)`` map used by the side-name resolver."""
161180
out: Dict[int, Tuple[str, str]] = {}

hip4/aio/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
pip install hip4[async]
66
77
Main entry points:
8-
* :class:`AsyncHIP4Client` low-level async info / exchange / WebSocket client.
9-
* :class:`AsyncHIP4Adapter` high-level async facade.
8+
* :class:`AsyncHIP4Client` - low-level async info / exchange / WebSocket client.
9+
* :class:`AsyncHIP4Adapter` - high-level async facade.
1010
"""
1111

1212
from hip4.aio.account import AsyncAccountAdapter
@@ -20,6 +20,11 @@
2020
from hip4.aio.events import AsyncEventsAdapter, SideNameResolver
2121
from hip4.aio.market_data import AsyncMarketDataAdapter, Candle
2222
from hip4.aio.ramp import AsyncRampAdapter
23+
from hip4.aio.streams.perp_price_feed import (
24+
PerpPriceFeedOptions,
25+
PerpPriceFeedSnapshot,
26+
create_async_perp_price_feed,
27+
)
2328
from hip4.aio.streams.price_feed import (
2429
PriceFeedOptions,
2530
PriceFeedSnapshot,
@@ -63,6 +68,8 @@
6368
"MergeOutcomeParams",
6469
"MergeQuestionParams",
6570
"NegateOutcomeParams",
71+
"PerpPriceFeedOptions",
72+
"PerpPriceFeedSnapshot",
6673
"PriceFeedOptions",
6774
"PriceFeedSnapshot",
6875
"SendAssetParams",
@@ -78,6 +85,7 @@
7885
"WalletActionResult",
7986
"WithdrawParams",
8087
"create_async_hip4_adapter",
88+
"create_async_perp_price_feed",
8189
"create_async_price_feed",
8290
"format_prediction_price",
8391
]

hip4/aio/account.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Async account adapter positions, activity, balances, open orders, position subscription."""
1+
"""Async account adapter - positions, activity, balances, open orders, position subscription."""
22

33
from __future__ import annotations
44

hip4/aio/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Async auth adapter same surface as :class:`hip4.sync.AuthAdapter` but ``async``-flavoured.
1+
"""Async auth adapter - same surface as :class:`hip4.sync.AuthAdapter` but ``async``-flavoured.
22
33
The actual signer state is identical (sync or async); we only add ``async``
44
versions of the public methods so async callers don't need ``run_in_executor``.

hip4/aio/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Asynchronous Hyperliquid HIP-4 HTTP + WebSocket client.
22
33
Mirror of :mod:`hip4.sync.client` using ``httpx`` (HTTP) and ``websockets``
4-
(WebSocket). Both are optional dependencies install with ``pip install
4+
(WebSocket). Both are optional dependencies - install with ``pip install
55
hip4[async]``.
66
"""
77

hip4/aio/events.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44

55
import asyncio
66
import time
7-
from typing import Callable, Dict, List, Optional, Tuple, Union
7+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union
88

99
from hip4._events_core import (
1010
CATEGORIES,
11+
apply_outcome_created_to_side_names,
1112
build_events_from_meta,
1213
extract_side_names,
1314
merge_mids_into_events,
1415
)
1516
from hip4.aio.client import AsyncHIP4Client
1617
from hip4.market_classification import classify_all_outcomes
1718
from hip4.types.event import PredictionCategory, PredictionEvent
18-
from hip4.types.hl import HLSettledOutcome
19+
from hip4.types.hl import HLSettledOutcome, HLWsOutcomeMetaUpdate
1920
from hip4.types.hip4_market import (
2021
FetchMarketsParams,
2122
HIP4Market,
@@ -27,7 +28,7 @@
2728
__all__ = ["AsyncEventsAdapter", "SideNameResolver"]
2829

2930
SideNameResolver = Callable[[int], Optional[Tuple[str, str]]]
30-
"""Resolve side names for an outcome by ID (sync pure data lookup)."""
31+
"""Resolve side names for an outcome by ID (sync - pure data lookup)."""
3132

3233
CACHE_TTL_S = 30.0
3334

@@ -115,6 +116,39 @@ async def fetch_markets(
115116
async def fetch_settled_outcome(self, outcome_id: int) -> Optional[HLSettledOutcome]:
116117
return await self._client.fetch_settled_outcome(outcome_id)
117118

119+
# -- subscriptions -----------------------------------------------------
120+
121+
async def subscribe_outcome_meta_updates(
122+
self,
123+
on_data: Callable[[HLWsOutcomeMetaUpdate], Any],
124+
) -> Callable[[], Awaitable[None]]:
125+
"""Subscribe to live outcome-meta updates. See sync equivalent for details.
126+
127+
``on_data`` may be sync or a coroutine; coroutines are scheduled with
128+
``asyncio.create_task``.
129+
"""
130+
import asyncio # local import keeps cold-start lazy
131+
132+
def _route(raw: Any) -> None:
133+
if not isinstance(raw, list):
134+
return
135+
for update in raw:
136+
if not isinstance(update, dict):
137+
continue
138+
self._apply_meta_update(update)
139+
result = on_data(update)
140+
if asyncio.iscoroutine(result):
141+
asyncio.create_task(result)
142+
143+
return await self._client.subscribe({"type": "outcomeMetaUpdates"}, _route)
144+
145+
def _apply_meta_update(self, update: HLWsOutcomeMetaUpdate) -> None:
146+
if "outcomeCreated" in update:
147+
apply_outcome_created_to_side_names(self._side_names, update["outcomeCreated"])
148+
# Drop time-based caches so the next read refetches.
149+
self._events_cache = None
150+
self._meta_cache = None
151+
118152
# -- internals ---------------------------------------------------------
119153

120154
async def _load_events(self) -> List[PredictionEvent]:

0 commit comments

Comments
 (0)