game-api-client 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: game-api-client
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python client library for GAME trading API
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: api,trading,game,client
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
23
|
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
23
24
|
Requires-Dist: pydantic (>=2.10.0,<3.0.0)
|
|
@@ -131,6 +132,8 @@ GAME_API_VERIFY_SSL=false
|
|
|
131
132
|
- `market_zone`: Market zone like "10YCH-SWISSGRIDZ" (optional)
|
|
132
133
|
- `get_latest_ltp()` - Get latest LTP per contract across all zones
|
|
133
134
|
- Returns dict mapping `contract_id -> {price, volume, timestamp, market_zones, latest_zone}`
|
|
135
|
+
- `get_latest_available_ltp(duration=None)` - Get the newest LTP compatible with available cross-border capacity
|
|
136
|
+
- Falls back to the next eligible trade and uses the Swiss LTP when no cross-border capacity is available
|
|
134
137
|
|
|
135
138
|
### Order Management
|
|
136
139
|
- `get_orders(direction=None)` - Get orders with optional filter
|
|
@@ -143,7 +146,10 @@ GAME_API_VERIFY_SSL=false
|
|
|
143
146
|
- `order_id`: Order ID (handles both string and numeric formats)
|
|
144
147
|
|
|
145
148
|
### Spot Data
|
|
146
|
-
- `get_spot_data()` - Get spot market data
|
|
149
|
+
- `get_spot_data(area_name=None, date_from=None, date_to=None, limit=None)` - Get spot market data
|
|
150
|
+
- `area_name`: Area name, such as `CH-SGD` (optional; API default: `CH-SGD`)
|
|
151
|
+
- `date_from`, `date_to`: Date range in `YYYY-MM-DD` format (optional)
|
|
152
|
+
- `limit`: Maximum number of results (optional; API default: `500`)
|
|
147
153
|
- `get_spot_status()` - Get spot API status
|
|
148
154
|
|
|
149
155
|
## Usage Examples
|
|
@@ -103,6 +103,8 @@ GAME_API_VERIFY_SSL=false
|
|
|
103
103
|
- `market_zone`: Market zone like "10YCH-SWISSGRIDZ" (optional)
|
|
104
104
|
- `get_latest_ltp()` - Get latest LTP per contract across all zones
|
|
105
105
|
- Returns dict mapping `contract_id -> {price, volume, timestamp, market_zones, latest_zone}`
|
|
106
|
+
- `get_latest_available_ltp(duration=None)` - Get the newest LTP compatible with available cross-border capacity
|
|
107
|
+
- Falls back to the next eligible trade and uses the Swiss LTP when no cross-border capacity is available
|
|
106
108
|
|
|
107
109
|
### Order Management
|
|
108
110
|
- `get_orders(direction=None)` - Get orders with optional filter
|
|
@@ -115,7 +117,10 @@ GAME_API_VERIFY_SSL=false
|
|
|
115
117
|
- `order_id`: Order ID (handles both string and numeric formats)
|
|
116
118
|
|
|
117
119
|
### Spot Data
|
|
118
|
-
- `get_spot_data()` - Get spot market data
|
|
120
|
+
- `get_spot_data(area_name=None, date_from=None, date_to=None, limit=None)` - Get spot market data
|
|
121
|
+
- `area_name`: Area name, such as `CH-SGD` (optional; API default: `CH-SGD`)
|
|
122
|
+
- `date_from`, `date_to`: Date range in `YYYY-MM-DD` format (optional)
|
|
123
|
+
- `limit`: Maximum number of results (optional; API default: `500`)
|
|
119
124
|
- `get_spot_status()` - Get spot API status
|
|
120
125
|
|
|
121
126
|
## Usage Examples
|
|
@@ -4,10 +4,12 @@ Simple GAME API Client
|
|
|
4
4
|
A minimal client for the GAME trading API.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
7
9
|
import logging
|
|
8
10
|
import time
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from typing import Any
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any
|
|
11
13
|
|
|
12
14
|
import httpx
|
|
13
15
|
|
|
@@ -17,8 +19,6 @@ logger = logging.getLogger(__name__)
|
|
|
17
19
|
class GameAPIError(Exception):
|
|
18
20
|
"""Base exception for GAME API errors"""
|
|
19
21
|
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
22
|
|
|
23
23
|
# Valid order states
|
|
24
24
|
VALID_ORDER_STATES = ["displayed", "hidden"]
|
|
@@ -31,9 +31,8 @@ def get_default_tz_offset_hours() -> int:
|
|
|
31
31
|
Mirrors the frontend's getCurrentTimezoneOffset() logic.
|
|
32
32
|
Returns 1 for CET, 2 for CEST, 0 for UTC.
|
|
33
33
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
offset_seconds = (now - utc_now.replace(tzinfo=None)).total_seconds()
|
|
34
|
+
local_offset = datetime.now().astimezone().utcoffset()
|
|
35
|
+
offset_seconds = local_offset.total_seconds() if local_offset else 0
|
|
37
36
|
offset_hours = round(offset_seconds / 3600)
|
|
38
37
|
return max(0, min(2, offset_hours))
|
|
39
38
|
|
|
@@ -62,8 +61,8 @@ class GameAPI:
|
|
|
62
61
|
self.verify_ssl = verify_ssl
|
|
63
62
|
self.timeout = timeout
|
|
64
63
|
self.personal_token = token
|
|
65
|
-
self._access_token:
|
|
66
|
-
self._token_expires_at:
|
|
64
|
+
self._access_token: str | None = None
|
|
65
|
+
self._token_expires_at: float | None = None
|
|
67
66
|
|
|
68
67
|
if not token:
|
|
69
68
|
raise GameAPIError("Personal token is required")
|
|
@@ -116,8 +115,8 @@ class GameAPI:
|
|
|
116
115
|
self,
|
|
117
116
|
method: str,
|
|
118
117
|
endpoint: str,
|
|
119
|
-
params:
|
|
120
|
-
json_data:
|
|
118
|
+
params: dict[str, Any] | None = None,
|
|
119
|
+
json_data: dict[str, Any] | None = None,
|
|
121
120
|
) -> Any:
|
|
122
121
|
"""Make an authenticated request to the API"""
|
|
123
122
|
url = f"{self.base_url}/api/v1{endpoint}"
|
|
@@ -146,7 +145,7 @@ class GameAPI:
|
|
|
146
145
|
try:
|
|
147
146
|
error_detail = response.json()
|
|
148
147
|
error_msg += f" - {error_detail.get('detail', 'Unknown error')}"
|
|
149
|
-
except
|
|
148
|
+
except (AttributeError, TypeError, ValueError):
|
|
150
149
|
error_msg += f" - {response.text}"
|
|
151
150
|
raise GameAPIError(error_msg)
|
|
152
151
|
|
|
@@ -156,14 +155,14 @@ class GameAPI:
|
|
|
156
155
|
raise GameAPIError(f"Network error: {e}") from e
|
|
157
156
|
|
|
158
157
|
# Status endpoints
|
|
159
|
-
async def get_status(self) ->
|
|
158
|
+
async def get_status(self) -> dict[str, Any]:
|
|
160
159
|
"""Get API status"""
|
|
161
160
|
return await self._make_request("GET", "/status")
|
|
162
161
|
|
|
163
162
|
# Trades endpoints
|
|
164
163
|
async def get_trades(
|
|
165
|
-
self, direction:
|
|
166
|
-
) ->
|
|
164
|
+
self, direction: str | None = None, duration: str | None = None
|
|
165
|
+
) -> list[dict[str, Any]]:
|
|
167
166
|
"""Get trades with optional filtering"""
|
|
168
167
|
params = {}
|
|
169
168
|
if direction:
|
|
@@ -173,14 +172,14 @@ class GameAPI:
|
|
|
173
172
|
return await self._make_request("GET", "/trades", params=params)
|
|
174
173
|
|
|
175
174
|
# Orders endpoints
|
|
176
|
-
async def get_orders(self, direction:
|
|
175
|
+
async def get_orders(self, direction: str | None = None) -> list[dict[str, Any]]:
|
|
177
176
|
"""Get orders with optional filtering"""
|
|
178
177
|
params = {}
|
|
179
178
|
if direction:
|
|
180
179
|
params["direction"] = direction
|
|
181
180
|
return await self._make_request("GET", "/orders", params=params)
|
|
182
181
|
|
|
183
|
-
async def find_order_by_id(self, order_id: str) ->
|
|
182
|
+
async def find_order_by_id(self, order_id: str) -> dict[str, Any] | None:
|
|
184
183
|
"""
|
|
185
184
|
Find a specific order by its ID (string or numeric)
|
|
186
185
|
|
|
@@ -201,11 +200,11 @@ class GameAPI:
|
|
|
201
200
|
):
|
|
202
201
|
return order
|
|
203
202
|
return None
|
|
204
|
-
except
|
|
203
|
+
except (GameAPIError, AttributeError, TypeError, ValueError) as e:
|
|
205
204
|
logger.error(f"Error finding order {order_id}: {e}")
|
|
206
205
|
return None
|
|
207
206
|
|
|
208
|
-
async def create_order(self, order_data:
|
|
207
|
+
async def create_order(self, order_data: dict[str, Any]) -> dict[str, Any]:
|
|
209
208
|
"""
|
|
210
209
|
Create a new order
|
|
211
210
|
|
|
@@ -238,7 +237,7 @@ class GameAPI:
|
|
|
238
237
|
self._validate_single_order(order_data, "Single order")
|
|
239
238
|
return await self._make_request("POST", "/orders", json_data=order_data)
|
|
240
239
|
|
|
241
|
-
def _validate_single_order(self, order:
|
|
240
|
+
def _validate_single_order(self, order: dict[str, Any], context: str) -> None:
|
|
242
241
|
"""Validate a single order's required fields and apply defaults"""
|
|
243
242
|
required_fields = ["side", "price", "volume", "date", "start_time", "end_time"]
|
|
244
243
|
# Optional fields that are passed through to the API
|
|
@@ -313,7 +312,7 @@ class GameAPI:
|
|
|
313
312
|
f"{context}: 'trader_suffixe' must be 50 characters or less"
|
|
314
313
|
)
|
|
315
314
|
|
|
316
|
-
async def delete_order(self, order_id: str) ->
|
|
315
|
+
async def delete_order(self, order_id: str) -> dict[str, Any]:
|
|
317
316
|
"""
|
|
318
317
|
Delete an order by ID
|
|
319
318
|
|
|
@@ -344,7 +343,7 @@ class GameAPI:
|
|
|
344
343
|
numeric_id = None
|
|
345
344
|
|
|
346
345
|
# Method 1: Check for explicit numeric ID field
|
|
347
|
-
if
|
|
346
|
+
if matching_order.get("id"):
|
|
348
347
|
numeric_id = str(matching_order["id"])
|
|
349
348
|
# Method 2: Check order_id_numeric field
|
|
350
349
|
elif "order_id_numeric" in matching_order:
|
|
@@ -393,7 +392,7 @@ class GameAPI:
|
|
|
393
392
|
return await self._make_request("DELETE", f"/orders/{numeric_id}")
|
|
394
393
|
|
|
395
394
|
# Market endpoints
|
|
396
|
-
def _sort_market_data(self, data:
|
|
395
|
+
def _sort_market_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
397
396
|
"""
|
|
398
397
|
Sort a market snapshot by contract/timestamp, then by direction,
|
|
399
398
|
placing the "best" price first for each side.
|
|
@@ -402,7 +401,7 @@ class GameAPI:
|
|
|
402
401
|
- buy/bid : higher price is better -> descending
|
|
403
402
|
"""
|
|
404
403
|
|
|
405
|
-
def _sort_key(row:
|
|
404
|
+
def _sort_key(row: dict[str, Any]):
|
|
406
405
|
contract = row.get("contract") or row.get("timestamp") or ""
|
|
407
406
|
direction = row.get("direction") or ""
|
|
408
407
|
price = row.get("price")
|
|
@@ -416,10 +415,10 @@ class GameAPI:
|
|
|
416
415
|
|
|
417
416
|
async def get_market_data(
|
|
418
417
|
self,
|
|
419
|
-
direction:
|
|
420
|
-
duration:
|
|
418
|
+
direction: str | None = None,
|
|
419
|
+
duration: str | None = None,
|
|
421
420
|
sort_by_price: bool = False,
|
|
422
|
-
) ->
|
|
421
|
+
) -> list[dict[str, Any]]:
|
|
423
422
|
"""
|
|
424
423
|
Get market data.
|
|
425
424
|
|
|
@@ -429,7 +428,7 @@ class GameAPI:
|
|
|
429
428
|
sort_by_price: If True, sort the result so the best offer for each
|
|
430
429
|
direction comes first (sell ascending, buy descending).
|
|
431
430
|
"""
|
|
432
|
-
params:
|
|
431
|
+
params: dict[str, Any] = {}
|
|
433
432
|
if direction:
|
|
434
433
|
params["direction"] = direction
|
|
435
434
|
if duration:
|
|
@@ -444,9 +443,9 @@ class GameAPI:
|
|
|
444
443
|
|
|
445
444
|
async def get_market_best_offers(
|
|
446
445
|
self,
|
|
447
|
-
duration:
|
|
448
|
-
top_n:
|
|
449
|
-
) ->
|
|
446
|
+
duration: str | None = None,
|
|
447
|
+
top_n: int | None = None,
|
|
448
|
+
) -> dict[str, dict[str, list[dict[str, Any]]]]:
|
|
450
449
|
"""
|
|
451
450
|
Get market offers grouped by contract and direction, best prices first.
|
|
452
451
|
|
|
@@ -468,7 +467,7 @@ class GameAPI:
|
|
|
468
467
|
}
|
|
469
468
|
"""
|
|
470
469
|
data = await self.get_market_data(duration=duration, sort_by_price=True)
|
|
471
|
-
grouped:
|
|
470
|
+
grouped: dict[str, dict[str, list[dict[str, Any]]]] = {}
|
|
472
471
|
for row in data:
|
|
473
472
|
contract = row.get("contract") or row.get("timestamp") or ""
|
|
474
473
|
direction = row.get("direction") or "unknown"
|
|
@@ -483,8 +482,8 @@ class GameAPI:
|
|
|
483
482
|
|
|
484
483
|
# LTP (Last Traded Price) endpoints
|
|
485
484
|
async def get_market_ltp(
|
|
486
|
-
self, market_zone:
|
|
487
|
-
) ->
|
|
485
|
+
self, market_zone: str | None = None
|
|
486
|
+
) -> list[dict[str, Any]]:
|
|
488
487
|
"""
|
|
489
488
|
Get last traded prices (LTP) for market contracts.
|
|
490
489
|
|
|
@@ -512,7 +511,23 @@ class GameAPI:
|
|
|
512
511
|
params["market_zone"] = market_zone
|
|
513
512
|
return await self._make_request("GET", "/market/market-ltp", params=params)
|
|
514
513
|
|
|
515
|
-
async def
|
|
514
|
+
async def get_latest_available_ltp(
|
|
515
|
+
self, duration: str | None = None
|
|
516
|
+
) -> list[dict[str, Any]]:
|
|
517
|
+
"""Get the newest LTP compatible with available cross-border capacity.
|
|
518
|
+
|
|
519
|
+
The GAME API selects the newest eligible trade per contract, falling
|
|
520
|
+
back to an older trade when the newest foreign-zone LTP has no
|
|
521
|
+
bidirectional capacity with Switzerland. CH remains the fallback zone.
|
|
522
|
+
"""
|
|
523
|
+
params: dict[str, Any] = {}
|
|
524
|
+
if duration:
|
|
525
|
+
params["duration"] = duration
|
|
526
|
+
return await self._make_request(
|
|
527
|
+
"GET", "/market/latest-ltp", params=params or None
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
async def get_latest_ltp(self) -> dict[str, dict[str, Any]]:
|
|
516
531
|
"""
|
|
517
532
|
Get the latest LTP for each contract across all market zones.
|
|
518
533
|
|
|
@@ -545,7 +560,7 @@ class GameAPI:
|
|
|
545
560
|
return {}
|
|
546
561
|
|
|
547
562
|
# Group by contract and keep the most recent entry per contract
|
|
548
|
-
latest_by_contract:
|
|
563
|
+
latest_by_contract: dict[str, dict[str, Any]] = {}
|
|
549
564
|
|
|
550
565
|
for entry in ltp_data:
|
|
551
566
|
contract = entry.get("contract")
|
|
@@ -571,21 +586,39 @@ class GameAPI:
|
|
|
571
586
|
latest_by_contract[contract]["market_zones"].add(zone)
|
|
572
587
|
|
|
573
588
|
# Convert sets to lists for JSON serialization
|
|
574
|
-
result:
|
|
589
|
+
result: dict[str, dict[str, Any]] = {}
|
|
575
590
|
for contract, data in latest_by_contract.items():
|
|
576
591
|
result[contract] = {
|
|
577
592
|
**data,
|
|
578
|
-
"market_zones": sorted(
|
|
593
|
+
"market_zones": sorted(data["market_zones"]),
|
|
579
594
|
}
|
|
580
595
|
|
|
581
596
|
return result
|
|
582
597
|
|
|
583
598
|
# Spot endpoints
|
|
584
|
-
async def get_spot_data(
|
|
585
|
-
|
|
599
|
+
async def get_spot_data(
|
|
600
|
+
self,
|
|
601
|
+
area_name: str | None = None,
|
|
602
|
+
date_from: str | None = None,
|
|
603
|
+
date_to: str | None = None,
|
|
604
|
+
limit: int | None = None,
|
|
605
|
+
) -> list[dict[str, Any]]:
|
|
606
|
+
"""Get spot data with optional area, date range, and result limit filters."""
|
|
607
|
+
params = {
|
|
608
|
+
key: value
|
|
609
|
+
for key, value in {
|
|
610
|
+
"area_name": area_name,
|
|
611
|
+
"date_from": date_from,
|
|
612
|
+
"date_to": date_to,
|
|
613
|
+
"limit": limit,
|
|
614
|
+
}.items()
|
|
615
|
+
if value is not None
|
|
616
|
+
}
|
|
617
|
+
if params:
|
|
618
|
+
return await self._make_request("GET", "/spot", params=params)
|
|
586
619
|
return await self._make_request("GET", "/spot")
|
|
587
620
|
|
|
588
|
-
async def get_spot_status(self) ->
|
|
621
|
+
async def get_spot_status(self) -> dict[str, Any]:
|
|
589
622
|
"""Get spot API status"""
|
|
590
623
|
return await self._make_request("GET", "/spot/status")
|
|
591
624
|
|
|
@@ -595,6 +628,6 @@ class GameAPI:
|
|
|
595
628
|
try:
|
|
596
629
|
await self.get_status()
|
|
597
630
|
return True
|
|
598
|
-
except
|
|
631
|
+
except GameAPIError as e:
|
|
599
632
|
logger.error(f"Connection test failed: {e}")
|
|
600
633
|
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "game-api-client"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Python client library for GAME trading API"
|
|
5
5
|
authors = ["Groupe E <support@groupe-e.ch>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -51,18 +51,18 @@ build-backend = "poetry.core.masonry.api"
|
|
|
51
51
|
|
|
52
52
|
[tool.black]
|
|
53
53
|
line-length = 88
|
|
54
|
-
target-version = "0.1.
|
|
54
|
+
target-version = "0.1.2"
|
|
55
55
|
|
|
56
56
|
[tool.ruff]
|
|
57
57
|
line-length = 88
|
|
58
|
-
target-version = "0.1.
|
|
58
|
+
target-version = "0.1.2"
|
|
59
59
|
|
|
60
60
|
[tool.ruff.lint]
|
|
61
61
|
select = ["E", "F", "W", "I", "N", "B", "C90"]
|
|
62
62
|
ignore = ["E501", "B008", "N999", "C901"]
|
|
63
63
|
|
|
64
64
|
[tool.mypy]
|
|
65
|
-
python_version = "0.1.
|
|
65
|
+
python_version = "0.1.2"
|
|
66
66
|
warn_return_any = true
|
|
67
67
|
warn_unused_configs = true
|
|
68
68
|
disallow_untyped_defs = true
|