Skip to content
Open
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
4 changes: 4 additions & 0 deletions .openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ sdk/open_api/models/account.py
sdk/open_api/models/account_balance.py
sdk/open_api/models/account_type.py
sdk/open_api/models/asset_definition.py
sdk/open_api/models/cancel_all_after_request.py
sdk/open_api/models/cancel_all_after_response.py
sdk/open_api/models/cancel_order_request.py
sdk/open_api/models/cancel_order_response.py
sdk/open_api/models/candle_history_data.py
Expand All @@ -33,6 +35,8 @@ sdk/open_api/models/market_definition.py
sdk/open_api/models/market_summary.py
sdk/open_api/models/mass_cancel_request.py
sdk/open_api/models/mass_cancel_response.py
sdk/open_api/models/modify_order_request.py
sdk/open_api/models/modify_order_response.py
sdk/open_api/models/order.py
sdk/open_api/models/order_status.py
sdk/open_api/models/order_type.py
Expand Down
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ python -m examples.websocket.market_monitoring
python -m examples.rpc.trade_execution
```

## Test tree layout (one axis per directory)
* `tests/engine/` — market-AGNOSTIC matching-engine behavior, every test parametrized `[spot, perp]` via the root-conftest `market_config`/`maker`/`taker` fixtures (lazy: a one-market env never spins up the other market's sessions). This is the home for ALL shared engine behavior including the feature suites — lifecycle (GTC/IOC/cancel/SMP), plus `test_modify_*`, `test_cod_*`, `test_post_only_*`. Select a feature with its marker: `-m modify` / `-m cod` / `-m post_only`. Fill-producing modules assert settlement via the injected `settlement_probe` (spot balance deltas / perp position deltas; `tests/helpers/settlement.py`) and wire per-market cleanup via the autouse `settlement_cleanup_guard`.
* `tests/spot/` — spot PHYSICS only (balance deltas, conservation, spotExecutions surfaces, pre-trade balance checks). `tests/perp/` — perp physics (baseline-relative positions, reduce-only, trigger orders).
* `tests/api_contract/` — raw EIP-712/nonce/deadline envelope validation; live but never trades (no balance/position guards). Pinned to the spot market + a 2-test perp cross-market smoke.
* `tests/ws_exec/` — WS order-entry transport (session semantics, error envelopes).
* `tests/parity/` + `tests/validation/` — OFFLINE (no network), selectable via `pytest -m offline` (also by path, unchanged).
* Selection: `-m spot` / `-m perp` are auto-derived from param ids + hand markers; `-m modify|cod|post_only` cross-cut the feature tests in `engine/` (and their offline guard twins in `validation/`).

## Testing against devnet
* The integration suites (`tests/test_orderbook`, `test_perps`, `test_spot`, `ws_exec`) run **live against devnet** — they place real orders, fill, settle on-chain, and assert on executions/balances.
* The live suites (`tests/engine`, `tests/spot`, `tests/perp`, `tests/api_contract`, `tests/ws_exec`) run **live against devnet** — they place real orders, fill, settle on-chain, and assert on executions/balances.
* **Before running the suite, kill any long-running example scripts** (e.g. `examples.websocket.perps.depth_market_maker`, any `python -m examples.*`). They maintain resting orders / open positions on the shared devnet test accounts and **pollute test state** — symptoms include `cancelledCount` mismatches, "reduce-only not rejected" (a leftover position exists), and matching against the wrong counterparty. Check with `ps -Ao pid,etime,command | grep -iE "examples\.|market_maker"` and kill stragglers before a run.
* Tests share a small pool of devnet accounts; leftover orders from a crashed/aborted run can also pollute — a clean run starts from no resting orders / no open positions on the test accounts.

Expand Down
2 changes: 1 addition & 1 deletion examples/rest_api/perps/markets_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async def main():

# Get markets configuration
print("\n--- Getting markets configuration ---")
config = await client.reference.get_market_definitions()
config = await client.reference.get_perp_market_definitions()
print(f"Markets configuration: {config}")

symbol = "ETHRUSDPERP"
Expand Down
29 changes: 18 additions & 11 deletions examples/rest_api/spot/create_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
"""
Create Orders - Creates a buy and sell order at extreme prices.

This script demonstrates how to place GTC limit orders that will sit in the order book:
This script demonstrates how to place resting limit orders that sit in the order book:
1. BUY order at $10 (far below market - will not fill immediately)
2. SELL order at $1,000,000 (far above market - will not fill immediately)

Both orders are for 0.001 ETH.
Both orders are for 0.001 ETH. The time-in-force is derived from
``ORDER_EXPIRY_SECONDS``: an expiry makes them GTT (rest, then auto-expire at
``expires_after``); ``None`` makes them GTC (rest until explicitly cancelled).

Requirements:
- CHAIN_ID: The chain ID (1729 for mainnet, 89346162 for testnet)
Expand Down Expand Up @@ -40,7 +42,10 @@
TRADE_QTY = "0.0001"
BUY_PRICE = "1" # $1 - safely below any realistic ETH bid, will rest
SELL_PRICE = "1000000" # $1M - safely above any realistic ETH ask, will rest
ORDER_EXPIRY_SECONDS = 120 # Orders expire after 120s; set to None for the SDK default (24h on spot GTC)
# An expiry makes the orders GTT (auto-expire after this many seconds); it must
# be longer than the ~60s signature deadline. Set to None for a true GTC order
# that rests until you cancel it.
ORDER_EXPIRY_SECONDS = 120

# =============================================================================
# LOGGING SETUP
Expand Down Expand Up @@ -94,10 +99,12 @@ async def main() -> None:
logger.info(f"Buy Price: ${BUY_PRICE}")
logger.info(f"Sell Price: ${SELL_PRICE}")
logger.info(f"Account ID: {account_id}")
# An expiry => GTT (rest, then auto-expire); no expiry => GTC (rest forever).
order_tif = TimeInForce.GTT if ORDER_EXPIRY_SECONDS else TimeInForce.GTC
if ORDER_EXPIRY_SECONDS:
logger.info(f"Order Expiry: {ORDER_EXPIRY_SECONDS}s per order")
logger.info(f"Time-in-force: GTT (auto-expires {ORDER_EXPIRY_SECONDS}s after placement)")
else:
logger.info("Order Expiry: default (24h)")
logger.info("Time-in-force: GTC (rests until cancelled)")
logger.info("=" * 60)

def expires_now() -> int | None:
Expand All @@ -114,32 +121,32 @@ def expires_now() -> int | None:
await client.start()
logger.info("✅ Client initialized")

# Place GTC BUY order
# Place BUY order
logger.info("-" * 60)
logger.info(f"📈 Placing GTC BUY order: {TRADE_QTY} ETH @ ${BUY_PRICE}")
logger.info(f"📈 Placing {order_tif.value} BUY order: {TRADE_QTY} ETH @ ${BUY_PRICE}")

buy_params = LimitOrderParameters(
symbol=SPOT_SYMBOL,
is_buy=True,
qty=TRADE_QTY,
limit_px=BUY_PRICE,
time_in_force=TimeInForce.GTC,
time_in_force=order_tif,
expires_after=expires_now(),
)

buy_response = await client.create_limit_order(buy_params)
logger.info(f"✅ BUY order placed: Order ID = {buy_response.order_id}")

# Place GTC SELL order
# Place SELL order
logger.info("-" * 60)
logger.info(f"📉 Placing GTC SELL order: {TRADE_QTY} ETH @ ${SELL_PRICE}")
logger.info(f"📉 Placing {order_tif.value} SELL order: {TRADE_QTY} ETH @ ${SELL_PRICE}")

sell_params = LimitOrderParameters(
symbol=SPOT_SYMBOL,
is_buy=False,
qty=TRADE_QTY,
limit_px=SELL_PRICE,
time_in_force=TimeInForce.GTC,
time_in_force=order_tif,
expires_after=expires_now(),
)

Expand Down
23 changes: 11 additions & 12 deletions examples/rest_api/spot/verify_rate_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import asyncio
import logging
import time

from dotenv import load_dotenv

Expand Down Expand Up @@ -146,7 +145,7 @@ async def test_b_open_order_count_cap():
logger.info("Waiting 65s for rate limit window to reset after cleanup...")
await asyncio.sleep(65)

expires_after = int(time.time()) + 300 # 5 min expiry
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)

# Place 3 GTC orders — all should succeed
logger.info("Placing 3 GTC orders (should all succeed)...")
Expand Down Expand Up @@ -208,7 +207,7 @@ async def test_b_open_order_count_cap():
await asyncio.sleep(65)

logger.info("Placing new GTC order after cancel (should succeed — 2 resting, cap is 3)...")
expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)
params = LimitOrderParameters(
symbol=SYMBOL,
is_buy=True,
Expand Down Expand Up @@ -269,7 +268,7 @@ async def test_c_rate_limit_shared_across_operations():
logger.info("Waiting 65s for rate limit window to reset...")
await asyncio.sleep(65)

expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)
order_ids = []

# Step 1: Place 2 GTC orders (2/5 rate limit, 2/3 cap)
Expand Down Expand Up @@ -417,7 +416,7 @@ async def test_d_mass_cancel_separate_bucket():
logger.info("Waiting 65s for both rate limit windows to reset...")
await asyncio.sleep(65)

expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)

# Step 1: Place 2 GTC orders
logger.info("")
Expand Down Expand Up @@ -451,7 +450,7 @@ async def test_d_mass_cancel_separate_bucket():
# Step 3: Place 2 GTC orders
logger.info("")
logger.info("Step 3: Placing 2 GTC orders...")
expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)
for i in range(1, 3):
params = LimitOrderParameters(
symbol=SYMBOL,
Expand Down Expand Up @@ -581,7 +580,7 @@ async def test_e_whitelisted_higher_cap():
logger.info("Wallet 1 (regular) — cap should be 3")
logger.info("-" * 60)

expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)

logger.info("Placing 3 GTC orders (should all succeed)...")
for i in range(1, 4):
Expand Down Expand Up @@ -624,7 +623,7 @@ async def test_e_whitelisted_higher_cap():
logger.info("Wallet 2 (whitelisted) — cap should be 5")
logger.info("-" * 60)

expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)

logger.info("Placing 5 GTC orders (should all succeed — whitelisted cap is 5)...")
for i in range(1, 6):
Expand Down Expand Up @@ -711,7 +710,7 @@ async def test_f_open_notional_cap():
logger.info("Waiting 65s for rate limit window to reset...")
await asyncio.sleep(65)

expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)

# Step 1: Place GTC buy at $1 for qty 2000 → notional = $2000 (under $3k cap)
logger.info("")
Expand Down Expand Up @@ -838,7 +837,7 @@ async def test_g_premium_wallet():
logger.info("Waiting 65s for rate limit windows to reset...")
await asyncio.sleep(65)

expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)

# Step 1+2: place 6 GTC orders, each notional = $1 × 1000 = $1000
# Totals: 6 orders (above regular count cap 3) and $6k notional
Expand Down Expand Up @@ -981,7 +980,7 @@ async def test_h_premium_mass_cancel():

mass_cancels_accepted = 0
for i in range(1, 5):
expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)
params = LimitOrderParameters(
symbol=SYMBOL,
is_buy=True,
Expand Down Expand Up @@ -1033,7 +1032,7 @@ async def test_h_premium_mass_cancel():
# Step 3: independent order bucket — place another GTC order
logger.info("")
logger.info("Step 3: Placing one more GTC order (order bucket should still have headroom — 4/7)")
expires_after = int(time.time()) + 300
expires_after = 0 # true GTC: rests until cancelled (probe cancels explicitly)
params = LimitOrderParameters(
symbol=SYMBOL,
is_buy=True,
Expand Down
27 changes: 13 additions & 14 deletions examples/websocket/perps/depth_market_maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@
# Settle asset on Reya is rUSD across all envs at the time of writing.
COLLATERAL_ASSET = "RUSD"

# GTC orders are signed with a long-lived `expires_after` so the matching
# engine doesn't quietly cancel resting depth before the next replace cycle.
# 10 minutes is well above any single cycle's batch of placements + WS
# round-trip slack, and at the off-chain api's documented deadline cap.
GTC_LIFETIME_S = 60 * 10
# Resting depth is posted as GTT (Good-Till-Time): it rests like GTC but the
# matching engine auto-reaps it at `expires_after`, so a stale quote is cleaned
# up if a replace cycle is missed. 10 minutes is well above any single cycle's
# batch of placements + WS round-trip slack. (A true GTC would rest forever
# until explicitly cancelled — `expires_after=0`.)
GTT_LIFETIME_S = 60 * 10


@dataclass
Expand Down Expand Up @@ -417,7 +418,7 @@ def on_close(self, _ws: ReyaSocket, close_status_code: int, close_msg: str) -> N

async def fetch_market_definition(client: ReyaTradingClient, symbol: str) -> MarketParams:
"""Look up perp market params via ``/marketDefinitions``."""
definitions = await client.reference.get_market_definitions()
definitions = await client.reference.get_perp_market_definitions()
for market in definitions:
if market.symbol == symbol:
return MarketParams(
Expand Down Expand Up @@ -511,16 +512,15 @@ async def place_single_order(

for attempt in range(max_retries):
try:
deadline = int(time.time()) + GTC_LIFETIME_S
expires_after = int(time.time()) + GTT_LIFETIME_S
await client.create_limit_order(
LimitOrderParameters(
symbol=symbol,
is_buy=is_buy,
limit_px=price,
qty=qty,
time_in_force=TimeInForce.GTC,
expires_after=deadline,
deadline=deadline,
time_in_force=TimeInForce.GTT,
expires_after=expires_after,
)
)
logger.info(f" Placed {side} @ ${price} qty={qty}")
Expand Down Expand Up @@ -628,16 +628,15 @@ async def cancel_and_replace_order(
qty_to_use = new_qty
for attempt in range(max_retries):
try:
deadline = int(time.time()) + GTC_LIFETIME_S
expires_after = int(time.time()) + GTT_LIFETIME_S
await client.create_limit_order(
LimitOrderParameters(
symbol=symbol,
is_buy=order.is_buy,
limit_px=str(new_price),
qty=qty_to_use,
time_in_force=TimeInForce.GTC,
expires_after=deadline,
deadline=deadline,
time_in_force=TimeInForce.GTT,
expires_after=expires_after,
)
)
return True
Expand Down
25 changes: 12 additions & 13 deletions examples/websocket/spot/depth_market_maker.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@
STATE_REFRESH_CYCLES = 30 # Refresh state from REST every N cycles to handle WS disconnects
MIN_BASE_BALANCE = Decimal("0.1") # Minimum ETH balance - stop MM if below this

# GTC orders are signed with a long-lived `expires_after` so the matching
# engine doesn't quietly cancel resting depth before the next replace cycle.
# 10 minutes is well above any single cycle's batch of placements + WS
# round-trip slack, and at the off-chain api's documented deadline cap.
GTC_LIFETIME_S = 60 * 10
# Resting depth is posted as GTT (Good-Till-Time): it rests like GTC but the
# matching engine auto-reaps it at `expires_after`, so a stale quote is cleaned
# up if a replace cycle is missed. 10 minutes is well above any single cycle's
# batch of placements + WS round-trip slack. (A true GTC would rest forever
# until explicitly cancelled — `expires_after=0`.)
GTT_LIFETIME_S = 60 * 10


@dataclass
Expand Down Expand Up @@ -545,16 +546,15 @@ async def place_single_order(

for attempt in range(max_retries):
try:
deadline = int(time.time()) + GTC_LIFETIME_S
expires_after = int(time.time()) + GTT_LIFETIME_S
await client.create_limit_order(
LimitOrderParameters(
symbol=symbol,
is_buy=is_buy,
limit_px=price,
qty=qty,
time_in_force=TimeInForce.GTC,
expires_after=deadline,
deadline=deadline,
time_in_force=TimeInForce.GTT,
expires_after=expires_after,
)
)
logger.info(f" Adding {side} @ ${price} qty={qty}")
Expand Down Expand Up @@ -727,16 +727,15 @@ async def cancel_and_replace_order(
qty_to_use = new_qty
for attempt in range(max_retries):
try:
deadline = int(time.time()) + GTC_LIFETIME_S
expires_after = int(time.time()) + GTT_LIFETIME_S
await client.create_limit_order(
LimitOrderParameters(
symbol=symbol,
is_buy=order.is_buy,
limit_px=str(new_price),
qty=qty_to_use,
time_in_force=TimeInForce.GTC,
expires_after=deadline,
deadline=deadline,
time_in_force=TimeInForce.GTT,
expires_after=expires_after,
)
)
return True
Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "reya-python-sdk"
version = "3.0.1.0"
version = "3.0.5.0"
description = "SDK for interacting with Reya Labs APIs"
authors = [
{name = "Reya Labs"}
Expand Down Expand Up @@ -168,10 +168,8 @@ markers = [
"spot: Spot trading tests (ETHRUSD, BTCRUSD)",
"perp: Perpetual trading tests (ETHRUSDPERP, BTCRUSDPERP)",
"market_data: Market data endpoint tests",
"slow: Tests that take longer than 5 seconds",
"maker_taker: Tests requiring two accounts (maker and taker)",
"websocket: Tests that verify WebSocket events",
"rest: Tests that verify REST API responses",
"e2e: Full end-to-end flow tests",
"ioc: Immediate-Or-Cancel order tests",
"gtc: Good-Till-Cancelled order tests",
Expand All @@ -181,5 +179,9 @@ markers = [
"validation: API validation tests (signature, nonce, deadline, etc.)",
"error: Error handling tests",
"rest_api: REST API endpoint tests",
"bust: Deliberate spot execution bust tests",
"cod: Cancel-on-disconnect (cancelAllAfter dead-man's-switch) tests",
"modify: In-place order modification (modifyOrder) tests",
"post_only: Post-only (maker-only) order tests",
"gtt: Good-Till-Time (auto-expiring resting order) tests",
"offline: No-network tests (parity golden vectors + client guards) — safe without devnet/.env",
]
2 changes: 1 addition & 1 deletion scripts/probe_perp_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def main() -> None:
client = ReyaTradingClient()
await client.start()
try:
defs = await client.reference.get_market_definitions()
defs = await client.reference.get_perp_market_definitions()
perp_defs = [d for d in defs if d.symbol.endswith("PERP")]
print(f"Found {len(perp_defs)} perp market(s): {[d.symbol for d in perp_defs]}")

Expand Down
Loading
Loading