feat: flatten opportunity commands, add config management, fix completions
- Flatten opportunity into top-level portfolio and opportunity commands - Add interactive config get/set/key/secret with type coercion - Rewrite --doc to show TUI vs JSON schema per command - Unify agent mode output to JSON only - Make init prompt for API key/secret interactively - Fix coin tab completion alias binding - Fix set_config_value reading from wrong path - Fail loudly on invalid numeric config values Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ class CLITestCase(unittest.TestCase):
|
||||
self.assertIn("account", help_text)
|
||||
self.assertIn("buy", help_text)
|
||||
self.assertIn("sell", help_text)
|
||||
self.assertIn("portfolio", help_text)
|
||||
self.assertIn("opportunity", help_text)
|
||||
self.assertIn("--doc", help_text)
|
||||
|
||||
@@ -79,16 +80,24 @@ class CLITestCase(unittest.TestCase):
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(captured["payload"]["trade"]["status"], "DRY_RUN")
|
||||
|
||||
def test_doc_flag_prints_documentation(self):
|
||||
import io
|
||||
from unittest.mock import patch
|
||||
|
||||
def test_doc_flag_prints_tui_documentation(self):
|
||||
stdout = io.StringIO()
|
||||
with patch("sys.stdout", stdout):
|
||||
result = cli.main(["market", "tickers", "--doc"])
|
||||
self.assertEqual(result, 0)
|
||||
output = stdout.getvalue()
|
||||
self.assertIn("lastPrice", output)
|
||||
self.assertIn("TUI Output", output)
|
||||
self.assertIn("Last Price", output)
|
||||
self.assertIn("BTCUSDT", output)
|
||||
|
||||
def test_doc_flag_prints_json_documentation(self):
|
||||
stdout = io.StringIO()
|
||||
with patch("sys.stdout", stdout):
|
||||
result = cli.main(["market", "tickers", "--doc", "--agent"])
|
||||
self.assertEqual(result, 0)
|
||||
output = stdout.getvalue()
|
||||
self.assertIn("JSON Output", output)
|
||||
self.assertIn("last_price", output)
|
||||
self.assertIn("BTCUSDT", output)
|
||||
|
||||
def test_account_dispatches(self):
|
||||
@@ -122,6 +131,46 @@ class CLITestCase(unittest.TestCase):
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(captured["payload"]["returncode"], 0)
|
||||
|
||||
def test_portfolio_dispatches(self):
|
||||
captured = {}
|
||||
with (
|
||||
patch.object(
|
||||
cli, "load_config", return_value={"binance": {"spot_base_url": "https://test", "recv_window": 5000}, "market": {"default_quote": "USDT"}, "opportunity": {"top_n": 10}}
|
||||
),
|
||||
patch.object(cli, "get_binance_credentials", return_value={"api_key": "k", "api_secret": "s"}),
|
||||
patch.object(cli, "SpotBinanceClient"),
|
||||
patch.object(
|
||||
cli.opportunity_service, "analyze_portfolio", return_value={"scores": [{"asset": "BTC", "score": 0.75}]}
|
||||
),
|
||||
patch.object(
|
||||
cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload)
|
||||
),
|
||||
):
|
||||
result = cli.main(["portfolio"])
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(captured["payload"]["scores"][0]["asset"], "BTC")
|
||||
|
||||
def test_opportunity_dispatches(self):
|
||||
captured = {}
|
||||
with (
|
||||
patch.object(
|
||||
cli, "load_config", return_value={"binance": {"spot_base_url": "https://test", "recv_window": 5000}, "market": {"default_quote": "USDT"}, "opportunity": {"top_n": 10}}
|
||||
),
|
||||
patch.object(cli, "get_binance_credentials", return_value={"api_key": "k", "api_secret": "s"}),
|
||||
patch.object(cli, "SpotBinanceClient"),
|
||||
patch.object(
|
||||
cli.opportunity_service,
|
||||
"scan_opportunities",
|
||||
return_value={"opportunities": [{"symbol": "BTCUSDT", "score": 0.82}]},
|
||||
),
|
||||
patch.object(
|
||||
cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload)
|
||||
),
|
||||
):
|
||||
result = cli.main(["opportunity", "-s", "BTCUSDT", "ETHUSDT"])
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(captured["payload"]["opportunities"][0]["symbol"], "BTCUSDT")
|
||||
|
||||
def test_catlog_dispatches(self):
|
||||
captured = {}
|
||||
with (
|
||||
@@ -138,3 +187,64 @@ class CLITestCase(unittest.TestCase):
|
||||
self.assertEqual(captured["payload"]["offset"], 10)
|
||||
self.assertIn("entries", captured["payload"])
|
||||
self.assertEqual(captured["payload"]["total"], 1)
|
||||
|
||||
def test_config_get_dispatches(self):
|
||||
captured = {}
|
||||
with (
|
||||
patch.object(cli, "load_config", return_value={"binance": {"recv_window": 5000}}),
|
||||
patch.object(
|
||||
cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload)
|
||||
),
|
||||
):
|
||||
result = cli.main(["config", "get", "binance.recv_window"])
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(captured["payload"]["binance.recv_window"], 5000)
|
||||
|
||||
def test_config_set_dispatches(self):
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
|
||||
f.write('[binance]\nrecv_window = 5000\n')
|
||||
tmp_path = f.name
|
||||
|
||||
with patch.object(cli, "get_runtime_paths") as mock_paths:
|
||||
mock_paths.return_value.config_file = __import__("pathlib").Path(tmp_path)
|
||||
result = cli.main(["config", "set", "binance.recv_window", "10000"])
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
# Verify the file was updated
|
||||
content = __import__("pathlib").Path(tmp_path).read_text()
|
||||
self.assertIn("recv_window = 10000", content)
|
||||
__import__("os").unlink(tmp_path)
|
||||
|
||||
def test_config_key_dispatches(self):
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f:
|
||||
f.write("BINANCE_API_KEY=\n")
|
||||
tmp_path = f.name
|
||||
|
||||
with patch.object(cli, "get_runtime_paths") as mock_paths:
|
||||
mock_paths.return_value.env_file = __import__("pathlib").Path(tmp_path)
|
||||
result = cli.main(["config", "key", "test_key_value"])
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
content = __import__("pathlib").Path(tmp_path).read_text()
|
||||
self.assertIn("BINANCE_API_KEY=test_key_value", content)
|
||||
__import__("os").unlink(tmp_path)
|
||||
|
||||
def test_config_secret_dispatches(self):
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f:
|
||||
f.write("BINANCE_API_SECRET=\n")
|
||||
tmp_path = f.name
|
||||
|
||||
with patch.object(cli, "get_runtime_paths") as mock_paths:
|
||||
mock_paths.return_value.env_file = __import__("pathlib").Path(tmp_path)
|
||||
result = cli.main(["config", "secret", "test_secret_value"])
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
content = __import__("pathlib").Path(tmp_path).read_text()
|
||||
self.assertIn("BINANCE_API_SECRET=test_secret_value", content)
|
||||
__import__("os").unlink(tmp_path)
|
||||
|
||||
Reference in New Issue
Block a user