fere-cli 0.1.0.dev6__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.
- fere_cli/__init__.py +3 -0
- fere_cli/async_util.py +63 -0
- fere_cli/banner.py +23 -0
- fere_cli/commands/__init__.py +0 -0
- fere_cli/commands/auth.py +108 -0
- fere_cli/commands/chat.py +198 -0
- fere_cli/commands/earn.py +143 -0
- fere_cli/commands/hooks.py +74 -0
- fere_cli/commands/limit_order.py +160 -0
- fere_cli/commands/portfolio.py +69 -0
- fere_cli/commands/swap.py +114 -0
- fere_cli/commands/utility.py +87 -0
- fere_cli/config.py +60 -0
- fere_cli/main.py +92 -0
- fere_cli/output.py +123 -0
- fere_cli-0.1.0.dev6.dist-info/METADATA +133 -0
- fere_cli-0.1.0.dev6.dist-info/RECORD +19 -0
- fere_cli-0.1.0.dev6.dist-info/WHEEL +4 -0
- fere_cli-0.1.0.dev6.dist-info/entry_points.txt +2 -0
fere_cli/__init__.py
ADDED
fere_cli/async_util.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Async bridge utilities for the CLI.
|
|
2
|
+
|
|
3
|
+
Click commands are synchronous; the SDK is async. This module
|
|
4
|
+
bridges the two with a thin ``run_async`` wrapper and a
|
|
5
|
+
``get_client`` factory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import sys
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
from fere_sdk import FereClient
|
|
17
|
+
|
|
18
|
+
from .config import load_config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_async(coro):
|
|
22
|
+
"""Run an async coroutine from synchronous context."""
|
|
23
|
+
return asyncio.run(coro)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def async_command(f: Callable) -> Callable:
|
|
27
|
+
"""Decorator: wraps an async function for use as a Click command."""
|
|
28
|
+
|
|
29
|
+
@wraps(f)
|
|
30
|
+
def wrapper(*args, **kwargs):
|
|
31
|
+
return run_async(f(*args, **kwargs))
|
|
32
|
+
|
|
33
|
+
return wrapper
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def get_client(ctx: click.Context) -> FereClient:
|
|
37
|
+
"""Create an authenticated FereClient from config + CLI overrides.
|
|
38
|
+
|
|
39
|
+
Raises click.ClickException if no agent name is configured.
|
|
40
|
+
"""
|
|
41
|
+
cfg = load_config()
|
|
42
|
+
|
|
43
|
+
# CLI flag overrides
|
|
44
|
+
agent_name = ctx.obj.get("agent_override") or cfg.get("agent_name")
|
|
45
|
+
base_url = ctx.obj.get("base_url_override") or cfg.get("base_url")
|
|
46
|
+
key_path = cfg.get("key_path")
|
|
47
|
+
|
|
48
|
+
if not agent_name:
|
|
49
|
+
raise click.ClickException(
|
|
50
|
+
"No agent configured. Run 'fere auth' first "
|
|
51
|
+
"or pass --agent NAME."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return await FereClient.create(
|
|
55
|
+
agent_name=agent_name,
|
|
56
|
+
base_url=base_url,
|
|
57
|
+
key_path=key_path,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_tty() -> bool:
|
|
62
|
+
"""Check if stdin is a TTY (interactive terminal)."""
|
|
63
|
+
return sys.stdin.isatty()
|
fere_cli/banner.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""ASCII art banner for the Fere CLI."""
|
|
2
|
+
|
|
3
|
+
from . import __version__
|
|
4
|
+
|
|
5
|
+
# Simplified spiral motif + stylized "FERE AI" text
|
|
6
|
+
# The spiral evokes the Fere AI logo's concentric swirl pattern
|
|
7
|
+
# Kept under 70 chars wide for comfortable 80-col terminal display
|
|
8
|
+
LOGO = r"""
|
|
9
|
+
. · .
|
|
10
|
+
· · _____ _____ ____ _____ _ ___
|
|
11
|
+
· . · . · | ___| ___| _ \| ____| / \ |_ _|
|
|
12
|
+
· · · · | |_ | |_ | |_) | _| / _ \ | |
|
|
13
|
+
· · . · · | _| | _| | _ <| |___ / ___ \ | |
|
|
14
|
+
· · · · |_| |___|_|_| \_\_____| /_/ \_\___|
|
|
15
|
+
· · . · ·
|
|
16
|
+
· ·
|
|
17
|
+
· . ·
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_banner() -> str:
|
|
22
|
+
"""Return the full banner string with version."""
|
|
23
|
+
return f"{LOGO}\n v{__version__} — crypto trading & research from your terminal\n"
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Auth commands: fere auth, fere whoami, fere credits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from fere_sdk import FereClient
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from ..async_util import async_command, get_client, is_tty, run_async
|
|
10
|
+
from ..config import load_config, save_config
|
|
11
|
+
from ..output import print_error, print_success
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.option("--name", prompt=False, default=None, help="Agent name.")
|
|
18
|
+
@click.pass_context
|
|
19
|
+
@async_command
|
|
20
|
+
async def auth(ctx, name: str | None):
|
|
21
|
+
"""Authenticate and register a new agent (first-run setup)."""
|
|
22
|
+
cfg = load_config()
|
|
23
|
+
|
|
24
|
+
agent_name = (
|
|
25
|
+
ctx.obj.get("agent_override")
|
|
26
|
+
or name
|
|
27
|
+
or cfg.get("agent_name")
|
|
28
|
+
)
|
|
29
|
+
if not agent_name:
|
|
30
|
+
if is_tty():
|
|
31
|
+
agent_name = click.prompt("Agent name")
|
|
32
|
+
else:
|
|
33
|
+
raise click.ClickException(
|
|
34
|
+
"Agent name required. Pass --name or --agent."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
base_url = ctx.obj.get("base_url_override") or cfg.get("base_url")
|
|
38
|
+
key_path = cfg.get("key_path")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
client = await FereClient.create(
|
|
42
|
+
agent_name=agent_name,
|
|
43
|
+
base_url=base_url,
|
|
44
|
+
key_path=key_path,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Persist agent name to config
|
|
48
|
+
cfg["agent_name"] = agent_name
|
|
49
|
+
save_config(cfg)
|
|
50
|
+
|
|
51
|
+
user = await client.get_user()
|
|
52
|
+
await client.close()
|
|
53
|
+
|
|
54
|
+
print_success(
|
|
55
|
+
{
|
|
56
|
+
"status": "authenticated",
|
|
57
|
+
"agent_name": agent_name,
|
|
58
|
+
"user": user,
|
|
59
|
+
},
|
|
60
|
+
quiet_value=agent_name,
|
|
61
|
+
)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print_error(f"Authentication failed: {e}")
|
|
64
|
+
raise SystemExit(1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@click.command()
|
|
68
|
+
@click.pass_context
|
|
69
|
+
@async_command
|
|
70
|
+
async def whoami(ctx):
|
|
71
|
+
"""Show current agent identity and wallet addresses."""
|
|
72
|
+
try:
|
|
73
|
+
client = await get_client(ctx)
|
|
74
|
+
user = await client.get_user()
|
|
75
|
+
wallets_data = await client.get_wallets()
|
|
76
|
+
await client.close()
|
|
77
|
+
|
|
78
|
+
result = {**user}
|
|
79
|
+
if wallets_data:
|
|
80
|
+
result["wallets"] = wallets_data
|
|
81
|
+
|
|
82
|
+
print_success(result)
|
|
83
|
+
except click.ClickException:
|
|
84
|
+
raise
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print_error(str(e))
|
|
87
|
+
raise SystemExit(1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@click.command()
|
|
91
|
+
@click.pass_context
|
|
92
|
+
@async_command
|
|
93
|
+
async def credits(ctx):
|
|
94
|
+
"""Show available credit balance."""
|
|
95
|
+
try:
|
|
96
|
+
client = await get_client(ctx)
|
|
97
|
+
balance = await client.get_credits()
|
|
98
|
+
await client.close()
|
|
99
|
+
|
|
100
|
+
print_success(
|
|
101
|
+
{"credits_available": balance},
|
|
102
|
+
quiet_value=str(balance),
|
|
103
|
+
)
|
|
104
|
+
except click.ClickException:
|
|
105
|
+
raise
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print_error(str(e))
|
|
108
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Chat commands: fere chat, fere threads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from ..async_util import async_command, get_client, is_tty
|
|
12
|
+
from ..output import (
|
|
13
|
+
is_json_mode,
|
|
14
|
+
print_error,
|
|
15
|
+
print_streaming_text,
|
|
16
|
+
print_success,
|
|
17
|
+
print_table_from_dicts,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command()
|
|
24
|
+
@click.argument("query", required=False, default=None)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--stream",
|
|
27
|
+
"stream_mode",
|
|
28
|
+
is_flag=True,
|
|
29
|
+
default=False,
|
|
30
|
+
help="Stream response events as they arrive.",
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"--thread",
|
|
34
|
+
"thread_id",
|
|
35
|
+
default=None,
|
|
36
|
+
help="Continue an existing chat thread.",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--agent",
|
|
40
|
+
"agent_type",
|
|
41
|
+
default="ProAgent",
|
|
42
|
+
help="Agent type to use (default: ProAgent).",
|
|
43
|
+
)
|
|
44
|
+
@click.pass_context
|
|
45
|
+
@async_command
|
|
46
|
+
async def chat(ctx, query, stream_mode, thread_id, agent_type):
|
|
47
|
+
"""Chat with FereAI's crypto AI agent.
|
|
48
|
+
|
|
49
|
+
Pass a query as an argument for one-shot mode, or run without
|
|
50
|
+
arguments to enter interactive REPL mode.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
client = await get_client(ctx)
|
|
54
|
+
except click.ClickException:
|
|
55
|
+
raise
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print_error(str(e))
|
|
58
|
+
raise SystemExit(1)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
if query:
|
|
62
|
+
# One-shot mode
|
|
63
|
+
if stream_mode:
|
|
64
|
+
await _stream_chat(
|
|
65
|
+
client, query, thread_id, agent_type
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
await _oneshot_chat(
|
|
69
|
+
client, query, thread_id, agent_type
|
|
70
|
+
)
|
|
71
|
+
elif is_tty():
|
|
72
|
+
# Interactive REPL
|
|
73
|
+
await _repl_chat(client, thread_id, agent_type)
|
|
74
|
+
else:
|
|
75
|
+
raise click.ClickException(
|
|
76
|
+
"Query required in non-interactive mode. "
|
|
77
|
+
"Usage: fere chat \"your question\""
|
|
78
|
+
)
|
|
79
|
+
except click.ClickException:
|
|
80
|
+
raise
|
|
81
|
+
except KeyboardInterrupt:
|
|
82
|
+
pass
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print_error(str(e))
|
|
85
|
+
raise SystemExit(1)
|
|
86
|
+
finally:
|
|
87
|
+
await client.close()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _oneshot_chat(client, query, thread_id, agent_type):
|
|
91
|
+
"""Send a query and print the final answer."""
|
|
92
|
+
result = await client.chat(
|
|
93
|
+
query, thread_id=thread_id, agent=agent_type
|
|
94
|
+
)
|
|
95
|
+
print_success(
|
|
96
|
+
result,
|
|
97
|
+
quiet_value=result.get("answer", ""),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def _stream_chat(client, query, thread_id, agent_type):
|
|
102
|
+
"""Stream chat events, printing each as it arrives."""
|
|
103
|
+
json_mode = is_json_mode()
|
|
104
|
+
|
|
105
|
+
async for event in client.chat_stream(
|
|
106
|
+
query, thread_id=thread_id, agent=agent_type
|
|
107
|
+
):
|
|
108
|
+
if json_mode:
|
|
109
|
+
# NDJSON: one JSON object per line
|
|
110
|
+
click.echo(
|
|
111
|
+
json.dumps(
|
|
112
|
+
{"event": event.event, "data": event.data},
|
|
113
|
+
default=str,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
if event.event == "chunk":
|
|
118
|
+
text = event.data.get("text", "")
|
|
119
|
+
print_streaming_text(text)
|
|
120
|
+
elif event.event == "answer":
|
|
121
|
+
text = event.data.get("text", "")
|
|
122
|
+
if text:
|
|
123
|
+
click.echo() # newline after chunks
|
|
124
|
+
console.print(
|
|
125
|
+
f"\n[bold]Answer:[/bold] {text}"
|
|
126
|
+
)
|
|
127
|
+
elif event.event == "tool_response":
|
|
128
|
+
console.print(
|
|
129
|
+
f"[dim]Tool: {json.dumps(event.data, default=str)}[/dim]"
|
|
130
|
+
)
|
|
131
|
+
elif event.event == "error":
|
|
132
|
+
msg = event.data.get("message", str(event.data))
|
|
133
|
+
print_error(msg)
|
|
134
|
+
elif event.event == "meta":
|
|
135
|
+
chat_id = event.data.get("chat_id", "")
|
|
136
|
+
console.print(f"[dim]Chat: {chat_id}[/dim]")
|
|
137
|
+
|
|
138
|
+
if not json_mode:
|
|
139
|
+
click.echo() # final newline
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def _repl_chat(client, thread_id, agent_type):
|
|
143
|
+
"""Interactive REPL loop."""
|
|
144
|
+
console.print(
|
|
145
|
+
"[bold]FereAI Chat[/bold] "
|
|
146
|
+
"[dim](type /exit or Ctrl+D to quit)[/dim]\n"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
current_thread = thread_id
|
|
150
|
+
|
|
151
|
+
while True:
|
|
152
|
+
try:
|
|
153
|
+
query = input(">>> ")
|
|
154
|
+
except EOFError:
|
|
155
|
+
click.echo()
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
query = query.strip()
|
|
159
|
+
if not query:
|
|
160
|
+
continue
|
|
161
|
+
if query in ("/exit", "/quit"):
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
result = await client.chat(
|
|
165
|
+
query, thread_id=current_thread, agent=agent_type
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Track thread for continuity
|
|
169
|
+
if "chat_id" in result and not current_thread:
|
|
170
|
+
current_thread = result["chat_id"]
|
|
171
|
+
|
|
172
|
+
answer = result.get("answer", "")
|
|
173
|
+
if answer:
|
|
174
|
+
console.print(f"\n{answer}\n")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@click.command()
|
|
178
|
+
@click.option(
|
|
179
|
+
"--limit",
|
|
180
|
+
default=10,
|
|
181
|
+
help="Number of threads to show.",
|
|
182
|
+
)
|
|
183
|
+
@click.option("--skip", default=0, help="Offset.")
|
|
184
|
+
@click.pass_context
|
|
185
|
+
@async_command
|
|
186
|
+
async def threads(ctx, limit, skip):
|
|
187
|
+
"""List recent chat threads."""
|
|
188
|
+
try:
|
|
189
|
+
client = await get_client(ctx)
|
|
190
|
+
data = await client.get_threads(skip=skip, limit=limit)
|
|
191
|
+
await client.close()
|
|
192
|
+
|
|
193
|
+
print_success(data)
|
|
194
|
+
except click.ClickException:
|
|
195
|
+
raise
|
|
196
|
+
except Exception as e:
|
|
197
|
+
print_error(str(e))
|
|
198
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Earn commands: fere earn (info, deposit, withdraw, positions)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from ..async_util import async_command, get_client, is_tty
|
|
9
|
+
from ..output import print_error, print_success
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group()
|
|
15
|
+
def earn():
|
|
16
|
+
"""Manage yield earning (deposit, withdraw, view APY)."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@earn.command()
|
|
21
|
+
@click.pass_context
|
|
22
|
+
@async_command
|
|
23
|
+
async def info(ctx):
|
|
24
|
+
"""Show current APY and vault details."""
|
|
25
|
+
try:
|
|
26
|
+
client = await get_client(ctx)
|
|
27
|
+
data = await client.get_earn_info()
|
|
28
|
+
await client.close()
|
|
29
|
+
|
|
30
|
+
print_success(data)
|
|
31
|
+
except click.ClickException:
|
|
32
|
+
raise
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print_error(str(e))
|
|
35
|
+
raise SystemExit(1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@earn.command()
|
|
39
|
+
@click.option(
|
|
40
|
+
"--amount",
|
|
41
|
+
required=True,
|
|
42
|
+
type=float,
|
|
43
|
+
help="USDC amount to deposit.",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--position-id",
|
|
47
|
+
default=None,
|
|
48
|
+
help="Existing position ID to add to.",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--timeout", default=120, type=int, help="Wait timeout (seconds)."
|
|
52
|
+
)
|
|
53
|
+
@click.option("-y", "--yes", is_flag=True, help="Skip confirmation.")
|
|
54
|
+
@click.pass_context
|
|
55
|
+
@async_command
|
|
56
|
+
async def deposit(ctx, amount, position_id, timeout, yes):
|
|
57
|
+
"""Deposit USDC to earn yield."""
|
|
58
|
+
if not yes and is_tty():
|
|
59
|
+
click.echo(f"Deposit {amount} USDC into Fere Earn")
|
|
60
|
+
if not click.confirm("Proceed?"):
|
|
61
|
+
raise SystemExit(0)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
client = await get_client(ctx)
|
|
65
|
+
|
|
66
|
+
with console.status("Processing deposit..."):
|
|
67
|
+
result = await client.deposit(
|
|
68
|
+
amount_usdc=amount,
|
|
69
|
+
position_id=position_id,
|
|
70
|
+
timeout=timeout,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
await client.close()
|
|
74
|
+
print_success(result)
|
|
75
|
+
except click.ClickException:
|
|
76
|
+
raise
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print_error(str(e))
|
|
79
|
+
raise SystemExit(1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@earn.command()
|
|
83
|
+
@click.option(
|
|
84
|
+
"--position-id",
|
|
85
|
+
required=True,
|
|
86
|
+
help="Position ID to withdraw from.",
|
|
87
|
+
)
|
|
88
|
+
@click.option(
|
|
89
|
+
"--amount",
|
|
90
|
+
required=True,
|
|
91
|
+
type=float,
|
|
92
|
+
help="USDC amount to withdraw.",
|
|
93
|
+
)
|
|
94
|
+
@click.option(
|
|
95
|
+
"--timeout", default=120, type=int, help="Wait timeout (seconds)."
|
|
96
|
+
)
|
|
97
|
+
@click.option("-y", "--yes", is_flag=True, help="Skip confirmation.")
|
|
98
|
+
@click.pass_context
|
|
99
|
+
@async_command
|
|
100
|
+
async def withdraw(ctx, position_id, amount, timeout, yes):
|
|
101
|
+
"""Withdraw USDC from a yield position."""
|
|
102
|
+
if not yes and is_tty():
|
|
103
|
+
click.echo(
|
|
104
|
+
f"Withdraw {amount} USDC from position {position_id}"
|
|
105
|
+
)
|
|
106
|
+
if not click.confirm("Proceed?"):
|
|
107
|
+
raise SystemExit(0)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
client = await get_client(ctx)
|
|
111
|
+
|
|
112
|
+
with console.status("Processing withdrawal..."):
|
|
113
|
+
result = await client.withdraw(
|
|
114
|
+
position_id=position_id,
|
|
115
|
+
amount_usdc=amount,
|
|
116
|
+
timeout=timeout,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
await client.close()
|
|
120
|
+
print_success(result)
|
|
121
|
+
except click.ClickException:
|
|
122
|
+
raise
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print_error(str(e))
|
|
125
|
+
raise SystemExit(1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@earn.command()
|
|
129
|
+
@click.pass_context
|
|
130
|
+
@async_command
|
|
131
|
+
async def positions(ctx):
|
|
132
|
+
"""Show active yield positions."""
|
|
133
|
+
try:
|
|
134
|
+
client = await get_client(ctx)
|
|
135
|
+
data = await client.get_positions()
|
|
136
|
+
await client.close()
|
|
137
|
+
|
|
138
|
+
print_success(data)
|
|
139
|
+
except click.ClickException:
|
|
140
|
+
raise
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print_error(str(e))
|
|
143
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Hooks commands: fere hooks set."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ..async_util import async_command, get_client
|
|
10
|
+
from ..output import print_error, print_success
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def hooks():
|
|
15
|
+
"""Manage stop-loss and take-profit hooks."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@hooks.command()
|
|
20
|
+
@click.option(
|
|
21
|
+
"--chain-id", required=True, type=int, help="Chain ID."
|
|
22
|
+
)
|
|
23
|
+
@click.option(
|
|
24
|
+
"--token", required=True, help="Token contract address."
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--stop-loss",
|
|
28
|
+
default=None,
|
|
29
|
+
help="Stop-loss config as JSON (e.g. '{\"price\": 1.5}').",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--take-profit",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Take-profit config as JSON (e.g. '{\"price\": 3.0}').",
|
|
35
|
+
)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
@async_command
|
|
38
|
+
async def set(ctx, chain_id, token, stop_loss, take_profit):
|
|
39
|
+
"""Set stop-loss and/or take-profit hooks on a token."""
|
|
40
|
+
try:
|
|
41
|
+
sl = json.loads(stop_loss) if stop_loss else None
|
|
42
|
+
except json.JSONDecodeError as exc:
|
|
43
|
+
raise click.BadParameter(
|
|
44
|
+
f"Invalid JSON for --stop-loss: {exc}", param_hint="'--stop-loss'"
|
|
45
|
+
)
|
|
46
|
+
try:
|
|
47
|
+
tp = json.loads(take_profit) if take_profit else None
|
|
48
|
+
except json.JSONDecodeError as exc:
|
|
49
|
+
raise click.BadParameter(
|
|
50
|
+
f"Invalid JSON for --take-profit: {exc}",
|
|
51
|
+
param_hint="'--take-profit'",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not sl and not tp:
|
|
55
|
+
raise click.ClickException(
|
|
56
|
+
"At least one of --stop-loss or --take-profit is required."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
client = await get_client(ctx)
|
|
61
|
+
result = await client.set_hooks(
|
|
62
|
+
chain_id=chain_id,
|
|
63
|
+
token_address=token,
|
|
64
|
+
stop_loss=sl,
|
|
65
|
+
take_profit=tp,
|
|
66
|
+
)
|
|
67
|
+
await client.close()
|
|
68
|
+
|
|
69
|
+
print_success(result)
|
|
70
|
+
except click.ClickException:
|
|
71
|
+
raise
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print_error(str(e))
|
|
74
|
+
raise SystemExit(1)
|