o2-cli 0.1.0__py3-none-any.whl
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.
- o2_cli/__init__.py +30 -0
- o2_cli/__main__.py +6 -0
- o2_cli/cli.py +94 -0
- o2_cli/client.py +307 -0
- o2_cli/commands/__init__.py +1 -0
- o2_cli/commands/_helpers.py +65 -0
- o2_cli/commands/account.py +46 -0
- o2_cli/commands/admin.py +147 -0
- o2_cli/commands/auth.py +140 -0
- o2_cli/commands/balance.py +64 -0
- o2_cli/commands/deposits.py +89 -0
- o2_cli/commands/fees.py +73 -0
- o2_cli/commands/markets.py +129 -0
- o2_cli/commands/mm.py +182 -0
- o2_cli/commands/notifications.py +136 -0
- o2_cli/commands/orders.py +331 -0
- o2_cli/commands/positions.py +158 -0
- o2_cli/commands/settings.py +129 -0
- o2_cli/commands/setup_cmd.py +78 -0
- o2_cli/commands/trades.py +86 -0
- o2_cli/commands/withdrawals.py +175 -0
- o2_cli/config.py +87 -0
- o2_cli/exceptions.py +31 -0
- o2_cli/output.py +224 -0
- o2_cli/setup.py +561 -0
- o2_cli-0.1.0.dist-info/METADATA +141 -0
- o2_cli-0.1.0.dist-info/RECORD +31 -0
- o2_cli-0.1.0.dist-info/WHEEL +5 -0
- o2_cli-0.1.0.dist-info/entry_points.txt +2 -0
- o2_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- o2_cli-0.1.0.dist-info/top_level.txt +1 -0
o2_cli/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""O2 CLI - Command-line interface for O2 DEX Trading Platform."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ensure_skill_installed() -> bool:
|
|
9
|
+
"""自动安装 Claude Code Skill 到 ~/.claude/skills/o2-cli/。
|
|
10
|
+
|
|
11
|
+
首次运行 o2 命令时检查并安装。不覆盖已有文件(用户可能自定义过)。
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
True 如果 skill 已存在或刚安装成功,False 如果安装失败。
|
|
15
|
+
"""
|
|
16
|
+
skill_dir = Path.home() / ".claude" / "skills" / "o2-cli"
|
|
17
|
+
skill_file = skill_dir / "SKILL.md"
|
|
18
|
+
|
|
19
|
+
if skill_file.exists():
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from o2_cli.setup import SKILL_CONTENT
|
|
24
|
+
|
|
25
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
skill_file.write_text(SKILL_CONTENT, encoding="utf-8")
|
|
27
|
+
return True
|
|
28
|
+
except OSError:
|
|
29
|
+
# 权限不足或其他 IO 错误,静默失败不影响 CLI 使用
|
|
30
|
+
return False
|
o2_cli/__main__.py
ADDED
o2_cli/cli.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""O2 CLI root - Typer application with global options."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing as t
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from o2_cli import __version__, ensure_skill_installed
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
name="o2",
|
|
13
|
+
help="O2 DEX Trading Platform CLI",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
rich_markup_mode="rich",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def version_callback(value: bool):
|
|
20
|
+
if value:
|
|
21
|
+
typer.echo(f"o2-cli version {__version__}")
|
|
22
|
+
raise typer.Exit()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.callback()
|
|
26
|
+
def main(
|
|
27
|
+
json_output: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
28
|
+
config: Optional[str] = typer.Option(None, "--config", help="Config file path"),
|
|
29
|
+
profile: Optional[str] = typer.Option(None, "--profile", help="Config profile"),
|
|
30
|
+
api_url: Optional[str] = typer.Option(None, "--api-url", help="Override API URL"),
|
|
31
|
+
timeout: float = typer.Option(30.0, "--timeout", help="HTTP timeout in seconds"),
|
|
32
|
+
verbose: bool = typer.Option(False, "-v", "--verbose", help="Verbose logging"),
|
|
33
|
+
version: bool = typer.Option(False, "--version", callback=version_callback, is_eager=True),
|
|
34
|
+
):
|
|
35
|
+
"""O2 DEX Trading Platform CLI."""
|
|
36
|
+
# Auto-install Claude Code skill on first run
|
|
37
|
+
ensure_skill_installed()
|
|
38
|
+
# Store global state for commands to access
|
|
39
|
+
app.state = {
|
|
40
|
+
"json_output": json_output,
|
|
41
|
+
"config_path": config,
|
|
42
|
+
"profile": profile,
|
|
43
|
+
"api_url": api_url,
|
|
44
|
+
"timeout": timeout,
|
|
45
|
+
"verbose": verbose,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_state() -> dict:
|
|
50
|
+
"""Get global CLI state."""
|
|
51
|
+
return getattr(app, "state", {
|
|
52
|
+
"json_output": False,
|
|
53
|
+
"config_path": None,
|
|
54
|
+
"profile": None,
|
|
55
|
+
"api_url": None,
|
|
56
|
+
"timeout": 30.0,
|
|
57
|
+
"verbose": False,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Register command groups
|
|
62
|
+
from o2_cli.commands import ( # noqa: E402
|
|
63
|
+
auth,
|
|
64
|
+
balance,
|
|
65
|
+
orders,
|
|
66
|
+
positions,
|
|
67
|
+
markets,
|
|
68
|
+
trades,
|
|
69
|
+
fees,
|
|
70
|
+
deposits,
|
|
71
|
+
withdrawals,
|
|
72
|
+
settings,
|
|
73
|
+
notifications,
|
|
74
|
+
account,
|
|
75
|
+
admin,
|
|
76
|
+
mm,
|
|
77
|
+
setup_cmd,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
app.add_typer(auth.app, name="auth", help="Authentication")
|
|
81
|
+
app.add_typer(balance.app, name="balance", help="Balance queries")
|
|
82
|
+
app.add_typer(orders.app, name="orders", help="Order management")
|
|
83
|
+
app.add_typer(positions.app, name="positions", help="Position management")
|
|
84
|
+
app.add_typer(markets.app, name="markets", help="Market data")
|
|
85
|
+
app.add_typer(trades.app, name="trades", help="Trade history")
|
|
86
|
+
app.add_typer(fees.app, name="fees", help="Fee information")
|
|
87
|
+
app.add_typer(deposits.app, name="deposits", help="Deposit management")
|
|
88
|
+
app.add_typer(withdrawals.app, name="withdrawals", help="Withdrawal management")
|
|
89
|
+
app.add_typer(settings.app, name="settings", help="User settings")
|
|
90
|
+
app.add_typer(notifications.app, name="notifications", help="Notifications")
|
|
91
|
+
app.add_typer(account.app, name="account", help="Account overview")
|
|
92
|
+
app.add_typer(admin.app, name="admin", help="Admin operations")
|
|
93
|
+
app.add_typer(mm.app, name="mm", help="Market maker control")
|
|
94
|
+
app.add_typer(setup_cmd.app, name="setup", help="Setup for vibe coding tools")
|
o2_cli/client.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Async httpx API client for O2 Backend.
|
|
2
|
+
|
|
3
|
+
All HTTP calls flow through this single class.
|
|
4
|
+
Automatically injects auth headers (JWT or API Key).
|
|
5
|
+
Handles O2Response envelope: {success, data, error, code, timestamp}.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from o2_cli.exceptions import APIError, ConnectionError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class O2Client:
|
|
16
|
+
"""Async HTTP client for O2 Backend API."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, base_url: str, timeout: float = 30.0):
|
|
19
|
+
self._base_url = base_url.rstrip("/")
|
|
20
|
+
self._timeout = timeout
|
|
21
|
+
self._token: Optional[str] = None
|
|
22
|
+
self._api_key_id: Optional[str] = None
|
|
23
|
+
self._api_secret: Optional[str] = None
|
|
24
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
25
|
+
|
|
26
|
+
async def __aenter__(self) -> "O2Client":
|
|
27
|
+
self._client = httpx.AsyncClient(
|
|
28
|
+
base_url=self._base_url,
|
|
29
|
+
timeout=self._timeout,
|
|
30
|
+
)
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
34
|
+
if self._client:
|
|
35
|
+
await self._client.aclose()
|
|
36
|
+
self._client = None
|
|
37
|
+
|
|
38
|
+
def set_jwt(self, token: str) -> None:
|
|
39
|
+
self._token = token
|
|
40
|
+
|
|
41
|
+
def set_api_key(self, key_id: str, secret: str) -> None:
|
|
42
|
+
self._api_key_id = key_id
|
|
43
|
+
self._api_secret = secret
|
|
44
|
+
|
|
45
|
+
def _build_headers(self) -> dict[str, str]:
|
|
46
|
+
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
47
|
+
if self._token:
|
|
48
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
49
|
+
if self._api_key_id and self._api_secret:
|
|
50
|
+
headers["X-API-Key-ID"] = self._api_key_id
|
|
51
|
+
headers["X-API-Secret"] = self._api_secret
|
|
52
|
+
return headers
|
|
53
|
+
|
|
54
|
+
async def _request(self, method: str, path: str, **kwargs) -> dict[str, Any]:
|
|
55
|
+
"""Core request method. Handles O2Response envelope."""
|
|
56
|
+
if not self._client:
|
|
57
|
+
raise RuntimeError("Client not initialized. Use 'async with O2Client(...)'")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
response = await self._client.request(
|
|
61
|
+
method, path, headers=self._build_headers(), **kwargs
|
|
62
|
+
)
|
|
63
|
+
except httpx.ConnectError as e:
|
|
64
|
+
raise ConnectionError(
|
|
65
|
+
f"Cannot connect to {self._base_url}. Is the O2 backend running?"
|
|
66
|
+
) from e
|
|
67
|
+
except httpx.TimeoutException as e:
|
|
68
|
+
raise ConnectionError(
|
|
69
|
+
f"Request timed out after {self._timeout}s. Try --timeout 60."
|
|
70
|
+
) from e
|
|
71
|
+
except httpx.HTTPError as e:
|
|
72
|
+
raise ConnectionError(f"Request failed: {e}") from e
|
|
73
|
+
|
|
74
|
+
# Parse response
|
|
75
|
+
try:
|
|
76
|
+
body = response.json()
|
|
77
|
+
except Exception:
|
|
78
|
+
if response.status_code >= 400:
|
|
79
|
+
raise APIError(response.status_code, response.text)
|
|
80
|
+
return {"data": response.text}
|
|
81
|
+
|
|
82
|
+
# Handle O2Response envelope
|
|
83
|
+
if isinstance(body, dict):
|
|
84
|
+
if "success" in body:
|
|
85
|
+
if not body["success"]:
|
|
86
|
+
error_msg = body.get("error") or body.get("message") or "Unknown error"
|
|
87
|
+
error_code = body.get("code")
|
|
88
|
+
raise APIError(response.status_code, error_msg, error_code)
|
|
89
|
+
data = body.get("data")
|
|
90
|
+
return data if data is not None else body
|
|
91
|
+
# Some endpoints return raw dicts without envelope
|
|
92
|
+
return body
|
|
93
|
+
|
|
94
|
+
return {"data": body}
|
|
95
|
+
|
|
96
|
+
async def get(self, path: str, params: dict | None = None) -> dict[str, Any]:
|
|
97
|
+
return await self._request("GET", path, params=params)
|
|
98
|
+
|
|
99
|
+
async def post(self, path: str, json: dict | None = None) -> dict[str, Any]:
|
|
100
|
+
return await self._request("POST", path, json=json or {})
|
|
101
|
+
|
|
102
|
+
async def put(self, path: str, json: dict | None = None) -> dict[str, Any]:
|
|
103
|
+
return await self._request("PUT", path, json=json or {})
|
|
104
|
+
|
|
105
|
+
async def delete(self, path: str) -> dict[str, Any]:
|
|
106
|
+
return await self._request("DELETE", path)
|
|
107
|
+
|
|
108
|
+
# ── Auth ──────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
async def auth_challenge(self, wallet_address: str) -> dict:
|
|
111
|
+
return await self.post("/auth/challenge", {"wallet_address": wallet_address})
|
|
112
|
+
|
|
113
|
+
async def auth_signature_login(
|
|
114
|
+
self, wallet_address: str, signature: str, message: str
|
|
115
|
+
) -> dict:
|
|
116
|
+
return await self.post(
|
|
117
|
+
"/auth/signature-login",
|
|
118
|
+
{
|
|
119
|
+
"wallet_address": wallet_address,
|
|
120
|
+
"signature": signature,
|
|
121
|
+
"message": message,
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def auth_test_login(self) -> dict:
|
|
126
|
+
return await self.post("/auth/test-login")
|
|
127
|
+
|
|
128
|
+
async def auth_me(self) -> dict:
|
|
129
|
+
return await self.get("/auth/me")
|
|
130
|
+
|
|
131
|
+
async def auth_session_status(self) -> dict:
|
|
132
|
+
return await self.get("/auth/session-status")
|
|
133
|
+
|
|
134
|
+
async def auth_refresh(self, token: str) -> dict:
|
|
135
|
+
self._token = token
|
|
136
|
+
return await self.post("/auth/refresh")
|
|
137
|
+
|
|
138
|
+
# ── Balance ───────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
async def get_balance(self) -> dict:
|
|
141
|
+
return await self.get("/balance")
|
|
142
|
+
|
|
143
|
+
async def get_balance_history(self, limit: int = 50, offset: int = 0) -> dict:
|
|
144
|
+
return await self.get("/balance/history", {"limit": limit, "offset": offset})
|
|
145
|
+
|
|
146
|
+
# ── Orders ────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
async def create_order(self, **kwargs) -> dict:
|
|
149
|
+
return await self.post("/orders", kwargs)
|
|
150
|
+
|
|
151
|
+
async def list_orders(
|
|
152
|
+
self, market_id: int | None = None, status_filter: str | None = None
|
|
153
|
+
) -> dict:
|
|
154
|
+
params = {}
|
|
155
|
+
if market_id is not None:
|
|
156
|
+
params["market_id"] = market_id
|
|
157
|
+
if status_filter:
|
|
158
|
+
params["status_filter"] = status_filter
|
|
159
|
+
return await self.get("/orders", params or None)
|
|
160
|
+
|
|
161
|
+
async def cancel_order(self, order_id: str) -> dict:
|
|
162
|
+
return await self.post("/orders/cancel", {"order_id": order_id})
|
|
163
|
+
|
|
164
|
+
async def cancel_all_orders(self, market_id: int | None = None) -> dict:
|
|
165
|
+
payload = {}
|
|
166
|
+
if market_id is not None:
|
|
167
|
+
payload["market_id"] = market_id
|
|
168
|
+
return await self.post("/orders/cancel-all", payload)
|
|
169
|
+
|
|
170
|
+
async def modify_order(self, order_id: str, **kwargs) -> dict:
|
|
171
|
+
kwargs["order_id"] = order_id
|
|
172
|
+
return await self.post("/orders/modify", kwargs)
|
|
173
|
+
|
|
174
|
+
async def batch_orders(self, operations: list) -> dict:
|
|
175
|
+
return await self.post("/orders/batch", {"operations": operations})
|
|
176
|
+
|
|
177
|
+
# ── Positions ─────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
async def get_positions(self) -> dict:
|
|
180
|
+
return await self.get("/positions")
|
|
181
|
+
|
|
182
|
+
async def get_position_by_market(self, market_id: int) -> dict:
|
|
183
|
+
return await self.get(f"/positions/market/{market_id}")
|
|
184
|
+
|
|
185
|
+
async def close_position(self, position_id: str) -> dict:
|
|
186
|
+
return await self.post(f"/positions/close/{position_id}")
|
|
187
|
+
|
|
188
|
+
async def get_liquidation_risk(self, market_id: int) -> dict:
|
|
189
|
+
return await self.get(f"/positions/liquidation-risk/{market_id}")
|
|
190
|
+
|
|
191
|
+
# ── Markets ───────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
async def get_markets(self) -> dict:
|
|
194
|
+
return await self.get("/markets")
|
|
195
|
+
|
|
196
|
+
async def get_orderbook(self, market_id: int) -> dict:
|
|
197
|
+
return await self.get(f"/markets/{market_id}/orderbook")
|
|
198
|
+
|
|
199
|
+
async def get_market_trades(self, market_id: int, limit: int = 50) -> dict:
|
|
200
|
+
return await self.get(f"/markets/{market_id}/trades", {"limit": limit})
|
|
201
|
+
|
|
202
|
+
async def get_candles(
|
|
203
|
+
self, market_id: int, interval: str = "1h", limit: int = 500
|
|
204
|
+
) -> dict:
|
|
205
|
+
return await self.get(
|
|
206
|
+
f"/markets/{market_id}/candles", {"interval": interval, "limit": limit}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# ── Trades ────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
async def get_trades(
|
|
212
|
+
self, skip: int = 0, limit: int = 50, market_id: int | None = None
|
|
213
|
+
) -> dict:
|
|
214
|
+
params = {"skip": skip, "limit": limit}
|
|
215
|
+
if market_id is not None:
|
|
216
|
+
params["market_id"] = market_id
|
|
217
|
+
return await self.get("/trades", params)
|
|
218
|
+
|
|
219
|
+
async def get_trade_summary(self) -> dict:
|
|
220
|
+
return await self.get("/trades/summary")
|
|
221
|
+
|
|
222
|
+
# ── Fees ──────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
async def get_fee_rates(self) -> dict:
|
|
225
|
+
return await self.get("/fees/rates")
|
|
226
|
+
|
|
227
|
+
async def estimate_fee(self, base_amount: str, price: str, is_maker: bool = False) -> dict:
|
|
228
|
+
return await self.post(
|
|
229
|
+
"/fees/estimate",
|
|
230
|
+
{"base_amount": base_amount, "price": price, "is_maker": is_maker},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# ── Deposits ──────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
async def get_deposit_address(self, chain: str = "base") -> dict:
|
|
236
|
+
return await self.get("/deposits/address", {"chain": chain})
|
|
237
|
+
|
|
238
|
+
async def get_deposit_history(self, limit: int = 50) -> dict:
|
|
239
|
+
return await self.get("/deposits/history", {"limit": limit})
|
|
240
|
+
|
|
241
|
+
# ── Withdrawals ───────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
async def create_withdrawal(
|
|
244
|
+
self, amount: str, address: str, chain: str = "ethereum", currency: str = "USDC"
|
|
245
|
+
) -> dict:
|
|
246
|
+
return await self.post(
|
|
247
|
+
"/withdrawals/create",
|
|
248
|
+
{
|
|
249
|
+
"amount": amount,
|
|
250
|
+
"to_address": address,
|
|
251
|
+
"chain": chain,
|
|
252
|
+
"currency": currency,
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
async def get_withdrawal(self, withdrawal_id: str) -> dict:
|
|
257
|
+
return await self.get(f"/withdrawals/{withdrawal_id}")
|
|
258
|
+
|
|
259
|
+
async def cancel_withdrawal(self, withdrawal_id: str) -> dict:
|
|
260
|
+
return await self.post(f"/withdrawals/{withdrawal_id}/cancel")
|
|
261
|
+
|
|
262
|
+
async def get_withdrawal_history(self, limit: int = 20, offset: int = 0) -> dict:
|
|
263
|
+
return await self.get("/withdrawals", {"limit": limit, "offset": offset})
|
|
264
|
+
|
|
265
|
+
# ── Settings ──────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
async def get_user_settings(self) -> dict:
|
|
268
|
+
return await self.get("/user-settings")
|
|
269
|
+
|
|
270
|
+
async def set_leverage(self, market_id: int, leverage: int) -> dict:
|
|
271
|
+
return await self.post(f"/user-settings/leverage/{market_id}", {"leverage": leverage})
|
|
272
|
+
|
|
273
|
+
async def set_margin_mode(self, mode: str) -> dict:
|
|
274
|
+
return await self.post("/user-settings/margin-mode", {"margin_mode": mode})
|
|
275
|
+
|
|
276
|
+
# ── Notifications ─────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
async def get_notifications(self, **kwargs) -> dict:
|
|
279
|
+
return await self.get("/notifications", kwargs or None)
|
|
280
|
+
|
|
281
|
+
async def get_unread_count(self) -> dict:
|
|
282
|
+
return await self.get("/notifications/unread-count")
|
|
283
|
+
|
|
284
|
+
async def mark_notification_read(self, notification_id: str) -> dict:
|
|
285
|
+
return await self.put(f"/notifications/{notification_id}/read")
|
|
286
|
+
|
|
287
|
+
# ── Account ───────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
async def get_account_overview(self) -> dict:
|
|
290
|
+
return await self.get("/account/overview")
|
|
291
|
+
|
|
292
|
+
# ── Market Maker ──────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
async def mm_status(self) -> dict:
|
|
295
|
+
return await self.get("/market-maker/status")
|
|
296
|
+
|
|
297
|
+
async def mm_start(self) -> dict:
|
|
298
|
+
return await self.post("/market-maker/start")
|
|
299
|
+
|
|
300
|
+
async def mm_stop(self) -> dict:
|
|
301
|
+
return await self.post("/market-maker/stop")
|
|
302
|
+
|
|
303
|
+
async def mm_stats(self) -> dict:
|
|
304
|
+
return await self.get("/market-maker-stats/overview")
|
|
305
|
+
|
|
306
|
+
async def mm_orders(self) -> dict:
|
|
307
|
+
return await self.get("/market-maker-orders/current")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""O2 CLI command groups."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Shared helpers for CLI commands to reduce boilerplate."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Callable, Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from o2_cli.cli import get_state
|
|
11
|
+
from o2_cli.client import O2Client
|
|
12
|
+
from o2_cli.config import load_config, get_active_profile, CONFIG_FILE
|
|
13
|
+
from o2_cli.exceptions import APIError, ConnectionError
|
|
14
|
+
from o2_cli.output import OutputFormatter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_context(state: dict | None = None):
|
|
18
|
+
"""Resolve CLI context: config, profile, formatter, api_url, timeout.
|
|
19
|
+
|
|
20
|
+
Returns: (profile, formatter, api_url, timeout)
|
|
21
|
+
"""
|
|
22
|
+
if state is None:
|
|
23
|
+
state = get_state()
|
|
24
|
+
|
|
25
|
+
config_path = Path(state["config_path"]) if state.get("config_path") else CONFIG_FILE
|
|
26
|
+
config = load_config(config_path)
|
|
27
|
+
if state.get("profile"):
|
|
28
|
+
config.active_profile = state["profile"]
|
|
29
|
+
|
|
30
|
+
profile = get_active_profile(config)
|
|
31
|
+
formatter = OutputFormatter(json_mode=state["json_output"])
|
|
32
|
+
api_url = state.get("api_url") or profile.api_url
|
|
33
|
+
timeout = state.get("timeout", profile.timeout) or profile.timeout
|
|
34
|
+
|
|
35
|
+
return profile, formatter, api_url, timeout
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def require_auth(profile, formatter) -> bool:
|
|
39
|
+
"""Check if user is authenticated. Returns True if OK, False if not."""
|
|
40
|
+
if profile.auth_type == "jwt" and not profile.token:
|
|
41
|
+
formatter.print_error("Not authenticated. Run 'o2 auth test-login' or 'o2 auth login' first.")
|
|
42
|
+
return False
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def setup_client(client: O2Client, profile):
|
|
47
|
+
"""Configure client auth from profile."""
|
|
48
|
+
if profile.token:
|
|
49
|
+
client.set_jwt(profile.token)
|
|
50
|
+
if profile.api_key_id:
|
|
51
|
+
client.set_api_key(profile.api_key_id, profile.api_secret)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def handle_api_errors(func: Callable) -> Callable:
|
|
55
|
+
"""Decorator for async command handlers that catches API/Connection errors."""
|
|
56
|
+
@wraps(func)
|
|
57
|
+
async def wrapper(*args, **kwargs):
|
|
58
|
+
try:
|
|
59
|
+
return await func(*args, **kwargs)
|
|
60
|
+
except APIError as e:
|
|
61
|
+
# Need to get formatter from somewhere - callers should handle
|
|
62
|
+
raise
|
|
63
|
+
except ConnectionError as e:
|
|
64
|
+
raise
|
|
65
|
+
return wrapper
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""o2 account commands - Account overview."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from o2_cli.cli import get_state
|
|
8
|
+
from o2_cli.client import O2Client
|
|
9
|
+
from o2_cli.config import load_config, get_active_profile
|
|
10
|
+
from o2_cli.exceptions import APIError, ConnectionError
|
|
11
|
+
from o2_cli.output import OutputFormatter
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Account overview")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("overview")
|
|
17
|
+
def overview():
|
|
18
|
+
"""Show full account overview (balance, positions, PnL)."""
|
|
19
|
+
asyncio.run(_overview())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _overview():
|
|
23
|
+
state = get_state()
|
|
24
|
+
formatter = OutputFormatter(json_mode=state["json_output"])
|
|
25
|
+
config = load_config()
|
|
26
|
+
profile = get_active_profile(config)
|
|
27
|
+
api_url = state.get("api_url") or profile.api_url
|
|
28
|
+
timeout = state.get("timeout") or profile.timeout
|
|
29
|
+
|
|
30
|
+
if not profile.token and profile.auth_type == "jwt":
|
|
31
|
+
formatter.print_error("Not authenticated. Run 'o2 auth test-login' first.")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
|
|
34
|
+
async with O2Client(api_url, timeout) as client:
|
|
35
|
+
client.set_jwt(profile.token)
|
|
36
|
+
if profile.api_key_id:
|
|
37
|
+
client.set_api_key(profile.api_key_id, profile.api_secret)
|
|
38
|
+
try:
|
|
39
|
+
data = await client.get_account_overview()
|
|
40
|
+
formatter.print_raw(data)
|
|
41
|
+
except APIError as e:
|
|
42
|
+
formatter.print_error(str(e), e.code)
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
except ConnectionError as e:
|
|
45
|
+
formatter.print_error(str(e))
|
|
46
|
+
raise typer.Exit(1)
|