Files
coinhunter/src/coinhunter/cli.py
Carlos Ouyang e4b2239bcd feat: add strategy and backtest services
- strategy_service.py combines opportunity + portfolio signals into
  unified buy/sell/hold recommendations
- backtest_service.py runs walk-forward backtests on historical datasets
  with virtual cash and positions
- CLI adds `strategy` and `backtest` commands with `--decision-interval`
  and other tuning parameters
- Add tests for both services and CLI dispatch
- Update CLAUDE.md with new architecture docs
- Optimize model weights via opportunity optimizer

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 13:21:35 +08:00

1372 lines
58 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""CoinHunter V2 CLI."""
from __future__ import annotations
import argparse
import sys
from typing import Any
from . import __version__
from .audit import read_audit_log
from .binance.spot_client import SpotBinanceClient
from .config import (
ensure_init_files,
get_binance_credentials,
get_config_value,
get_runtime_paths,
load_config,
set_config_value,
set_env_value,
)
from .runtime import (
install_shell_completion,
print_output,
self_upgrade,
with_spinner,
)
from .services import (
account_service,
backtest_service,
market_service,
opportunity_dataset_service,
opportunity_evaluation_service,
opportunity_service,
portfolio_service,
strategy_service,
trade_service,
)
EPILOG = """\
examples:
coin init
coin account
coin m tk BTCUSDT ETHUSDT
coin m k BTCUSDT -i 1h -l 50
coin buy BTCUSDT -Q 100 -d
coin sell BTCUSDT --qty 0.01 --type limit --price 90000
coin opportunity -s BTCUSDT ETHUSDT
coin opportunity evaluate ~/.coinhunter/datasets/opportunity_dataset.json --agent
coin opportunity optimize ~/.coinhunter/datasets/opportunity_dataset.json --agent
coin strategy -s BTCUSDT ETHUSDT
coin backtest ~/.coinhunter/datasets/opportunity_dataset_20260101T000000Z.json
coin upgrade
"""
COMMAND_DOCS: dict[str, dict[str, str]] = {
"init": {
"tui": """\
TUI Output:
INITIALIZED
Root: ~/.coinhunter
Config: ~/.coinhunter/config.toml
Env: ~/.coinhunter/.env
Logs: ~/.coinhunter/logs
Files created: config.toml, .env
✓ Shell completions installed for zsh
Path: ~/.zsh/completions/_coinhunter
Hint: Run 'compinit' or restart your terminal to activate completions
JSON Output:
{
"root": "~/.coinhunter",
"files_created": ["config.toml", ".env"],
"completion": {
"shell": "zsh",
"installed": true,
"path": "~/.zsh/completions/_coinhunter",
"hint": "Run 'compinit' or restart your terminal to activate completions"
}
}
Fields:
root runtime directory path
files_created list of generated files (e.g. ["config.toml", ".env"])
completion shell completion installation status (installed, path, hint)
""",
"json": """\
JSON Output:
{
"root": "~/.coinhunter",
"files_created": ["config.toml", ".env"],
"completion": {
"shell": "zsh",
"installed": true,
"path": "~/.zsh/completions/_coinhunter",
"hint": "Run 'compinit' or restart your terminal to activate completions"
}
}
Fields:
root runtime directory path
files_created list of generated files (e.g. ["config.toml", ".env"])
completion shell completion installation status (installed, path, hint)
""",
},
"account": {
"tui": """\
TUI Output:
┌─────────┬──────┬────────┬───────┬───────────────────┬──────┐
│ Asset │ Free │ Locked │ Total │ Notional (USDT) │ │
├─────────┼──────┼────────┼───────┼───────────────────┼──────┤
│ BTC │ 0.5 │ 0.1 │ 0.6 │ 30,000 │ │
│ ETH │ 2.0 │ 0.0 │ 2.0 │ 5,000 │ │
│ USDT │ 10 │ 0 │ 10 │ 10 │ dust │
└─────────┴──────┴────────┴───────┴───────────────────┴──────┘
JSON Output:
{
"balances": [
{"asset": "BTC", "free": 0.5, "locked": 0.1, "total": 0.6, "notional_usdt": 30000.0, "is_dust": false}
]
}
Fields:
asset asset symbol (e.g. "BTC", "ETH", "USDT")
free available balance (float)
locked frozen/locked balance (float)
total free + locked (float)
notional_usdt estimated value in USDT (float)
is_dust true if value is below dust threshold (bool)
""",
"json": """\
JSON Output:
{
"balances": [
{"asset": "BTC", "free": 0.5, "locked": 0.1, "total": 0.6, "notional_usdt": 30000.0, "is_dust": false}
]
}
Fields:
asset asset symbol (e.g. "BTC", "ETH", "USDT")
free available balance (float)
locked frozen/locked balance (float)
total free + locked (float)
notional_usdt estimated value in USDT (float)
is_dust true if value is below dust threshold (bool)
""",
},
"market/tickers": {
"tui": """\
TUI Output:
TICKERS window=1d
┌─────────┬────────────┬──────────┬──────────────┐
│ Symbol │ Last Price │ Change % │ Quote Volume │
├─────────┼────────────┼──────────┼──────────────┤
│ BTCUSDT │ 70,000 │ +2.50% │ 123,456 │
│ ETHUSDT │ 3,500 │ -1.20% │ 89,012 │
└─────────┴────────────┴──────────┴──────────────┘
JSON Output:
{
"tickers": [
{"symbol": "BTCUSDT", "last_price": 70000.0, "price_change_pct": 2.5, "quote_volume": 123456.0}
],
"window": "1d"
}
Fields:
symbol normalized trading pair (e.g. "BTCUSDT")
last_price latest traded price (float)
price_change_pct change % over the selected window (float, e.g. 2.5 = +2.5%)
quote_volume quote volume over the selected window (float)
window statistics window (enum: 1m, 2m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 2d, 3d, 5d, 7d, 15d, 30d)
""",
"json": """\
JSON Output:
{
"tickers": [
{"symbol": "BTCUSDT", "last_price": 70000.0, "price_change_pct": 2.5, "quote_volume": 123456.0}
],
"window": "1d"
}
Fields:
symbol normalized trading pair (e.g. "BTCUSDT")
last_price latest traded price (float)
price_change_pct change % over the selected window (float, e.g. 2.5 = +2.5%)
quote_volume quote volume over the selected window (float)
window statistics window (enum: 1m, 2m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 2d, 3d, 5d, 7d, 15d, 30d)
""",
},
"market/klines": {
"tui": """\
TUI Output:
KLINES interval=1h limit=100 count=100
┌─────────┬────────────┬────────┬────────┬────────┬────────┬───────┐
│ Symbol │ Time │ Open │ High │ Low │ Close │ Vol │
├─────────┼────────────┼────────┼────────┼────────┼────────┼───────┤
│ BTCUSDT │ 1713000000 │ 69,000 │ 69,500 │ 68,800 │ 69,200 │ 100.5 │
└─────────┴────────────┴────────┴────────┴────────┴────────┴───────┘
... and 99 more rows
JSON Output:
{
"interval": "1h",
"limit": 100,
"klines": [
{"symbol": "BTCUSDT", "interval": "1h", "open_time": 1713000000000, "open": 69000.0, "high": 69500.0,
"low": 68800.0, "close": 69200.0, "volume": 100.5, "close_time": 1713003600000, "quote_volume": 6950000.0}
]
}
Fields:
interval candle interval (enum: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M)
limit number of candles requested (int)
symbol normalized trading pair
open_time candle open timestamp in ms (int)
close_time candle close timestamp in ms (int)
open/high/low/close OHLC prices (float)
volume traded base volume (float)
quote_volume traded quote volume (float)
""",
"json": """\
JSON Output:
{
"interval": "1h",
"limit": 100,
"klines": [
{"symbol": "BTCUSDT", "interval": "1h", "open_time": 1713000000000, "open": 69000.0, "high": 69500.0,
"low": 68800.0, "close": 69200.0, "volume": 100.5, "close_time": 1713003600000, "quote_volume": 6950000.0}
]
}
Fields:
interval candle interval (enum: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M)
limit number of candles requested (int)
symbol normalized trading pair
open_time candle open timestamp in ms (int)
close_time candle close timestamp in ms (int)
open/high/low/close OHLC prices (float)
volume traded base volume (float)
quote_volume traded quote volume (float)
""",
},
"buy": {
"tui": """\
TUI Output:
TRADE RESULT
Market: SPOT
Symbol: BTCUSDT
Side: BUY
Type: MARKET
Status: DRY_RUN
Dry Run: true
JSON Output:
{
"trade": {
"market_type": "spot",
"symbol": "BTCUSDT",
"side": "BUY",
"order_type": "MARKET",
"status": "DRY_RUN",
"dry_run": true,
"request_payload": {"symbol": "BTCUSDT", "side": "BUY", "type": "MARKET", "quoteOrderQty": 100},
"response_payload": {"dry_run": true, "status": "DRY_RUN", "request": {...}}
}
}
Fields:
market_type always "spot"
side always "BUY"
order_type enum: "MARKET" | "LIMIT"
status enum: "DRY_RUN", "NEW", "PARTIALLY_FILLED", "FILLED", "CANCELED", "REJECTED", "EXPIRED"
dry_run true if simulated (bool)
request_payload normalized order sent to Binance
response_payload raw exchange response (or dry_run stub)
""",
"json": """\
JSON Output:
{
"trade": {
"market_type": "spot",
"symbol": "BTCUSDT",
"side": "BUY",
"order_type": "MARKET",
"status": "DRY_RUN",
"dry_run": true,
"request_payload": {"symbol": "BTCUSDT", "side": "BUY", "type": "MARKET", "quoteOrderQty": 100},
"response_payload": {"dry_run": true, "status": "DRY_RUN", "request": {...}}
}
}
Fields:
market_type always "spot"
side always "BUY"
order_type enum: "MARKET" | "LIMIT"
status enum: "DRY_RUN", "NEW", "PARTIALLY_FILLED", "FILLED", "CANCELED", "REJECTED", "EXPIRED"
dry_run true if simulated (bool)
request_payload normalized order sent to Binance
response_payload raw exchange response (or dry_run stub)
""",
},
"sell": {
"tui": """\
TUI Output:
TRADE RESULT
Market: SPOT
Symbol: BTCUSDT
Side: SELL
Type: LIMIT
Status: FILLED
Dry Run: false
JSON Output:
{
"trade": {
"market_type": "spot",
"symbol": "BTCUSDT",
"side": "SELL",
"order_type": "LIMIT",
"status": "FILLED",
"dry_run": false,
"request_payload": {"symbol": "BTCUSDT", "side": "SELL", "type": "LIMIT", "quantity": 0.01, "price": 90000, "timeInForce": "GTC"},
"response_payload": {"symbol": "BTCUSDT", "orderId": 12345, "status": "FILLED", ...}
}
}
Fields:
market_type always "spot"
side always "SELL"
order_type enum: "MARKET" | "LIMIT"
status enum: "DRY_RUN", "NEW", "PARTIALLY_FILLED", "FILLED", "CANCELED", "REJECTED", "EXPIRED"
dry_run true if simulated (bool)
request_payload normalized order sent to Binance
response_payload raw exchange response (or dry_run stub)
""",
"json": """\
JSON Output:
{
"trade": {
"market_type": "spot",
"symbol": "BTCUSDT",
"side": "SELL",
"order_type": "LIMIT",
"status": "FILLED",
"dry_run": false,
"request_payload": {"symbol": "BTCUSDT", "side": "SELL", "type": "LIMIT", "quantity": 0.01, "price": 90000, "timeInForce": "GTC"},
"response_payload": {"symbol": "BTCUSDT", "orderId": 12345, "status": "FILLED", ...}
}
}
Fields:
market_type always "spot"
side always "SELL"
order_type enum: "MARKET" | "LIMIT"
status enum: "DRY_RUN", "NEW", "PARTIALLY_FILLED", "FILLED", "CANCELED", "REJECTED", "EXPIRED"
dry_run true if simulated (bool)
request_payload normalized order sent to Binance
response_payload raw exchange response (or dry_run stub)
""",
},
"portfolio": {
"tui": """\
TUI Output:
RECOMMENDATIONS count=3
1. BTCUSDT action=add score=0.7500
· market signal is strong and position still has room
trend=1.0 momentum=0.02 breakout=0.85 volume_confirmation=1.2 volatility=0.01 position_weight=0.3
2. ETHUSDT action=hold score=0.6000
· market structure remains supportive for holding
trend=1.0 momentum=0.01 breakout=0.5 volume_confirmation=1.0 volatility=0.02 position_weight=0.2
3. SOLUSDT action=trim score=-0.2000
· position weight is above the portfolio risk budget
trend=-1.0 momentum=-0.01 breakout=0.3 volume_confirmation=0.8 volatility=0.03 position_weight=0.5
JSON Output:
{
"recommendations": [
{"symbol": "BTCUSDT", "action": "add", "score": 0.75, "reasons": ["market signal is strong and position still has room"],
"metrics": {"trend": 1.0, "momentum": 0.02, "breakout": 0.85, "volume_confirmation": 1.2, "volatility": 0.01, "position_weight": 0.3}}
]
}
Fields:
symbol trading pair (e.g. "BTCUSDT")
action enum: "add" | "hold" | "trim" | "exit" | "review"
score shared market signal score (float, can be negative)
reasons list of human-readable explanations
metrics scoring breakdown
trend -1.0 (down) | 0.0 (neutral) | 1.0 (up)
momentum price momentum signal (float)
breakout proximity to recent high, 0-1 (float)
volume_confirmation volume ratio vs average (float, >1 = above average)
volatility price range relative to current (float)
position_weight position weight in portfolio (float, 0-1)
""",
"json": """\
JSON Output:
{
"recommendations": [
{"symbol": "BTCUSDT", "action": "add", "score": 0.75, "reasons": ["market signal is strong and position still has room"],
"metrics": {"trend": 1.0, "momentum": 0.02, "breakout": 0.85, "volume_confirmation": 1.2, "volatility": 0.01, "position_weight": 0.3}}
]
}
Fields:
symbol trading pair (e.g. "BTCUSDT")
action enum: "add" | "hold" | "trim" | "exit" | "review"
score shared market signal score (float, can be negative)
reasons list of human-readable explanations
metrics scoring breakdown
trend -1.0 (down) | 0.0 (neutral) | 1.0 (up)
momentum price momentum signal (float)
breakout proximity to recent high, 0-1 (float)
volume_confirmation volume ratio vs average (float, >1 = above average)
volatility price range relative to current (float)
position_weight position weight in portfolio (float, 0-1)
""",
},
"opportunity": {
"tui": """\
TUI Output:
RECOMMENDATIONS count=5
1. ETHUSDT action=entry confidence=74 score=1.7200
· fresh breakout trigger with clean setup and manageable extension
· base asset ETH passed liquidity and tradability filters
setup_score=0.74 trigger_score=0.61 liquidity_score=1.0 extension_penalty=0.0 opportunity_score=1.72 position_weight=0.0
2. BTCUSDT action=watch confidence=52 score=0.7800
· setup is constructive but the trigger is not clean enough yet
· base asset BTC passed liquidity and tradability filters
· symbol is already held, so the opportunity score is discounted for overlap
setup_score=0.68 trigger_score=0.25 liquidity_score=1.0 extension_penalty=0.1 opportunity_score=0.96 position_weight=0.3
JSON Output:
{
"recommendations": [
{"symbol": "ETHUSDT", "action": "entry", "confidence": 74, "score": 1.72,
"reasons": ["fresh breakout trigger with clean setup and manageable extension", "base asset ETH passed liquidity and tradability filters"],
"metrics": {"setup_score": 0.74, "trigger_score": 0.61, "liquidity_score": 1.0, "extension_penalty": 0.0, "opportunity_score": 1.72, "position_weight": 0.0}}
]
}
Fields:
symbol trading pair (e.g. "ETHUSDT")
action enum: "entry" | "watch" | "avoid"
confidence 0..100 confidence index derived from edge_score
score opportunity score after extension and overlap/risk discounts
reasons list of human-readable explanations (includes liquidity filter note for scan)
metrics scoring breakdown
setup_score compression, higher-lows, and breakout-proximity quality
trigger_score fresh-breakout, volume, and controlled-momentum quality
liquidity_score relative quote-volume quality after liquidity filters
extension_penalty overextension/chase risk from run-up and MA distance
opportunity_score raw opportunity score before overlap discount
position_weight current portfolio overlap in the same symbol
""",
"json": """\
JSON Output:
{
"recommendations": [
{"symbol": "ETHUSDT", "action": "entry", "confidence": 74, "score": 1.72,
"reasons": ["fresh breakout trigger with clean setup and manageable extension", "base asset ETH passed liquidity and tradability filters"],
"metrics": {"setup_score": 0.74, "trigger_score": 0.61, "liquidity_score": 1.0, "extension_penalty": 0.0, "opportunity_score": 1.72, "position_weight": 0.0}}
]
}
Fields:
symbol trading pair (e.g. "ETHUSDT")
action enum: "entry" | "watch" | "avoid"
confidence 0..100 confidence index derived from edge_score
score opportunity score after extension and overlap/risk discounts
reasons list of human-readable explanations (includes liquidity filter note for scan)
metrics scoring breakdown
setup_score compression, higher-lows, and breakout-proximity quality
trigger_score fresh-breakout, volume, and controlled-momentum quality
liquidity_score relative quote-volume quality after liquidity filters
extension_penalty overextension/chase risk from run-up and MA distance
opportunity_score raw opportunity score before overlap discount
position_weight current portfolio overlap in the same symbol
""",
},
"opportunity/dataset": {
"tui": """\
TUI Output:
DATASET COLLECTED
Path: ~/.coinhunter/datasets/opportunity_dataset_20260421T120000Z.json
Symbols: BTCUSDT, ETHUSDT
Window: reference=48.0d simulate=7.0d run=7.0d
JSON Output:
{
"path": "~/.coinhunter/datasets/opportunity_dataset_20260421T120000Z.json",
"symbols": ["BTCUSDT", "ETHUSDT"],
"counts": {"BTCUSDT": {"1h": 1488}},
"plan": {"reference_days": 48.0, "simulate_days": 7.0, "run_days": 7.0, "total_days": 62.0},
"external_history": {"provider": "coingecko", "status": "available"}
}
Fields:
path JSON dataset file written locally
symbols symbols included in the dataset
counts kline row counts by symbol and interval
plan reference/simulation/run windows used for collection
external_history external provider historical capability probe result
""",
"json": """\
JSON Output:
{
"path": "~/.coinhunter/datasets/opportunity_dataset_20260421T120000Z.json",
"symbols": ["BTCUSDT", "ETHUSDT"],
"counts": {"BTCUSDT": {"1h": 1488}},
"plan": {"reference_days": 48.0, "simulate_days": 7.0, "run_days": 7.0, "total_days": 62.0},
"external_history": {"provider": "coingecko", "status": "available"}
}
Fields:
path JSON dataset file written locally
symbols symbols included in the dataset
counts kline row counts by symbol and interval
plan reference/simulation/run windows used for collection
external_history external provider historical capability probe result
""",
},
"opportunity/evaluate": {
"tui": """\
TUI Output:
SUMMARY
count=120 correct=76 incorrect=44 accuracy=0.6333
interval=1h top_n=10 decision_times=24
BY ACTION
trigger count=12 correct=7 accuracy=0.5833 avg_trade_return=0.0062
setup count=78 correct=52 accuracy=0.6667
skip count=30 correct=17 accuracy=0.5667
JSON Output:
{
"summary": {"count": 120, "correct": 76, "incorrect": 44, "accuracy": 0.6333},
"by_action": {"trigger": {"count": 12, "correct": 7, "accuracy": 0.5833}},
"trade_simulation": {"trigger_trades": 12, "wins": 7, "losses": 5, "win_rate": 0.5833},
"rules": {"horizon_hours": 24.0, "take_profit": 0.02, "stop_loss": 0.015, "setup_target": 0.01}
}
Fields:
summary aggregate walk-forward judgment accuracy
by_action accuracy and average returns grouped by trigger/setup/chase/skip
trade_simulation trigger-only trade outcome using take-profit/stop-loss rules
rules objective evaluation assumptions used for the run
examples first evaluated judgments with outcome labels
""",
"json": """\
JSON Output:
{
"summary": {"count": 120, "correct": 76, "incorrect": 44, "accuracy": 0.6333},
"by_action": {"trigger": {"count": 12, "correct": 7, "accuracy": 0.5833}},
"trade_simulation": {"trigger_trades": 12, "wins": 7, "losses": 5, "win_rate": 0.5833},
"rules": {"horizon_hours": 24.0, "take_profit": 0.02, "stop_loss": 0.015, "setup_target": 0.01}
}
Fields:
summary aggregate walk-forward judgment accuracy
by_action accuracy and average returns grouped by trigger/setup/chase/skip
trade_simulation trigger-only trade outcome using take-profit/stop-loss rules
rules objective evaluation assumptions used for the run
examples first evaluated judgments with outcome labels
""",
},
"opportunity/optimize": {
"tui": """\
TUI Output:
BASELINE
objective=0.5012 accuracy=0.5970 trigger_win_rate=0.4312
BEST
objective=0.5341 accuracy=0.6214 trigger_win_rate=0.4862
JSON Output:
{
"baseline": {"objective": 0.5012, "summary": {"accuracy": 0.597}},
"best": {"objective": 0.5341, "summary": {"accuracy": 0.6214}},
"improvement": {"accuracy": 0.0244, "trigger_win_rate": 0.055},
"recommended_config": {"opportunity.model_weights.trigger": 1.5}
}
Fields:
baseline evaluation snapshot with current model weights
best best walk-forward snapshot found by coordinate search
improvement deltas from baseline to best
recommended_config config keys that can be written with `coin config set`
search optimizer metadata; thresholds are fixed
""",
"json": """\
JSON Output:
{
"baseline": {"objective": 0.5012, "summary": {"accuracy": 0.597}},
"best": {"objective": 0.5341, "summary": {"accuracy": 0.6214}},
"improvement": {"accuracy": 0.0244, "trigger_win_rate": 0.055},
"recommended_config": {"opportunity.model_weights.trigger": 1.5}
}
Fields:
baseline evaluation snapshot with current model weights
best best walk-forward snapshot found by coordinate search
improvement deltas from baseline to best
recommended_config config keys that can be written with `coin config set`
search optimizer metadata; thresholds are fixed
""",
},
"upgrade": {
"tui": """\
TUI Output:
✓ Update completed
upgraded coinhunter 2.1.1 -> 2.2.0
(or on failure)
✗ Update failed (exit code 1)
error: could not find a version that satisfies the requirement
JSON Output:
{
"command": "pipx upgrade coinhunter",
"returncode": 0,
"stdout": "upgraded coinhunter 2.1.1 -> 2.2.0",
"stderr": ""
}
Fields:
command shell command executed (e.g. "pipx upgrade coinhunter" or "python -m pip install --upgrade coinhunter")
returncode process exit code (0 = success, non-zero = failure)
stdout command standard output
stderr command standard error
""",
"json": """\
JSON Output:
{
"command": "pipx upgrade coinhunter",
"returncode": 0,
"stdout": "upgraded coinhunter 2.1.1 -> 2.2.0",
"stderr": ""
}
Fields:
command shell command executed (e.g. "pipx upgrade coinhunter" or "python -m pip install --upgrade coinhunter")
returncode process exit code (0 = success, non-zero = failure)
stdout command standard output
stderr command standard error
""",
},
"catlog": {
"tui": """\
TUI Output:
AUDIT LOG
2026-04-20 12:00:00 trade_submitted
symbol=BTCUSDT side=BUY qty=0.01 order_type=MARKET status=submitted dry_run=true
2026-04-20 12:00:01 trade_filled
symbol=BTCUSDT side=BUY qty=0.01 order_type=MARKET status=FILLED dry_run=false
2026-04-20 12:05:00 opportunity_scan_generated
mode=scan symbols=["BTCUSDT", "ETHUSDT"]
JSON Output:
{
"entries": [
{"timestamp": "2026-04-20T12:00:00Z", "event": "trade_submitted",
"symbol": "BTCUSDT", "side": "BUY", "qty": 0.01, "order_type": "MARKET", "status": "submitted", "dry_run": true}
],
"limit": 10,
"offset": 0,
"total": 1
}
Fields:
entries list of audit events (fields vary by event type)
timestamp ISO 8601 timestamp
event enum: "trade_submitted", "trade_filled", "trade_failed",
"opportunity_portfolio_generated", "opportunity_scan_generated"
limit requested entry limit
offset skip offset
total number of entries returned
""",
"json": """\
JSON Output:
{
"entries": [
{"timestamp": "2026-04-20T12:00:00Z", "event": "trade_submitted",
"symbol": "BTCUSDT", "side": "BUY", "qty": 0.01, "order_type": "MARKET", "status": "submitted", "dry_run": true}
],
"limit": 10,
"offset": 0,
"total": 1
}
Fields:
entries list of audit events (fields vary by event type)
timestamp ISO 8601 timestamp
event enum: "trade_submitted", "trade_filled", "trade_failed",
"opportunity_portfolio_generated", "opportunity_scan_generated"
limit requested entry limit
offset skip offset
total number of entries returned
""",
},
"config/get": {
"tui": """\
TUI Output:
CONFIG
binance.recv_window = 5000
opportunity.top_n = 10
JSON Output:
{"binance.recv_window": 5000, "opportunity.top_n": 10}
Fields:
key dot-notation config key (e.g. "binance.recv_window")
value current value (type depends on key: bool, int, float, list, str)
""",
"json": """\
JSON Output:
{"binance.recv_window": 5000, "opportunity.top_n": 10}
Fields:
key dot-notation config key (e.g. "binance.recv_window")
value current value (type depends on key: bool, int, float, list, str)
""",
},
"config/set": {
"tui": """\
TUI Output:
CONFIG UPDATED
binance.recv_window = 10000
JSON Output:
{"updated": true, "key": "binance.recv_window", "value": 10000}
Fields:
updated true if the config file was written successfully
key dot-notation config key that was updated
value new value after type coercion
""",
"json": """\
JSON Output:
{"updated": true, "key": "binance.recv_window", "value": 10000}
Fields:
updated true if the config file was written successfully
key dot-notation config key that was updated
value new value after type coercion
""",
},
"config/key": {
"tui": """\
TUI Output:
API KEY SET
BINANCE_API_KEY = ***...abc
JSON Output:
{"key": "BINANCE_API_KEY", "set": true}
Fields:
key always "BINANCE_API_KEY"
set true if the value was written to ~/.coinhunter/.env
""",
"json": """\
JSON Output:
{"key": "BINANCE_API_KEY", "set": true}
Fields:
key always "BINANCE_API_KEY"
set true if the value was written to ~/.coinhunter/.env
""",
},
"config/secret": {
"tui": """\
TUI Output:
API SECRET SET
BINANCE_API_SECRET = ***...xyz
JSON Output:
{"key": "BINANCE_API_SECRET", "set": true}
Fields:
key always "BINANCE_API_SECRET"
set true if the value was written to ~/.coinhunter/.env
""",
"json": """\
JSON Output:
{"key": "BINANCE_API_SECRET", "set": true}
Fields:
key always "BINANCE_API_SECRET"
set true if the value was written to ~/.coinhunter/.env
""",
},
"completion": {
"tui": """\
TUI Output:
(raw shell script, not JSON)
#compdef coin
# AUTOMATICALLY GENERATED by `shtab`
...
JSON Output:
(same as TUI shell script text, not JSON)
""",
"json": """\
JSON Output:
(shell script text, not JSON suitable for sourcing in your shell rc file)
#compdef coin
# AUTOMATICALLY GENERATED by `shtab`
...
""",
},
}
def _load_spot_client(config: dict[str, Any], *, client: Any | None = None) -> SpotBinanceClient:
credentials = get_binance_credentials()
binance_config = config["binance"]
return SpotBinanceClient(
api_key=credentials["api_key"],
api_secret=credentials["api_secret"],
base_url=binance_config["spot_base_url"],
recv_window=int(binance_config["recv_window"]),
client=client,
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="coin",
description="Binance-first trading CLI for account balances, market data, trade execution, and opportunity scanning.",
epilog=EPILOG,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("-v", "--version", action="version", version=__version__)
parser.add_argument("-a", "--agent", action="store_true", help="Output in agent-friendly format (JSON or compact)")
parser.add_argument("--doc", action="store_true", help="Show output schema and field descriptions for the command")
subparsers = parser.add_subparsers(dest="command")
def _add_global_flags(p: argparse.ArgumentParser) -> None:
p.add_argument("-a", "--agent", action="store_true", help="Output in agent-friendly format (JSON or compact)")
p.add_argument("--doc", action="store_true", help="Show output schema and field descriptions for the command")
init_parser = subparsers.add_parser(
"init", help="Generate config.toml, .env, and log directory",
description="Initialize the CoinHunter runtime directory with config.toml, .env, and shell completions. "
"Interactively prompts for Binance API key and secret if missing.",
)
init_parser.add_argument("-f", "--force", action="store_true", help="Overwrite existing files")
init_parser.add_argument("--no-prompt", action="store_true", help="Skip interactive API key/secret prompt")
_add_global_flags(init_parser)
config_parser = subparsers.add_parser(
"config", aliases=["cfg", "c"], help="Manage configuration",
description="Read and write config.toml and .env settings without manual editing.",
)
config_subparsers = config_parser.add_subparsers(dest="config_command")
config_get_parser = config_subparsers.add_parser(
"get", help="Read a config value",
description="Read a value from config.toml using dot notation (e.g. binance.recv_window).",
)
config_get_parser.add_argument("key", nargs="?", help="Config key in dot notation (e.g. trading.dry_run_default)")
_add_global_flags(config_get_parser)
config_set_parser = config_subparsers.add_parser(
"set", help="Write a config value",
description="Write a value to config.toml using dot notation. "
"Booleans accept: true/1/yes/on or false/0/no/off. Lists use comma-separated values.",
)
config_set_parser.add_argument("key", help="Config key in dot notation (e.g. opportunity.top_n)")
config_set_parser.add_argument("value", help="Value to set")
_add_global_flags(config_set_parser)
config_key_parser = config_subparsers.add_parser(
"key", help="Set Binance API key",
description="Set BINANCE_API_KEY in the .env file. Prompts interactively if no value provided.",
)
config_key_parser.add_argument("value", nargs="?", help="API key value (omit to prompt interactively)")
_add_global_flags(config_key_parser)
config_secret_parser = config_subparsers.add_parser(
"secret", help="Set Binance API secret",
description="Set BINANCE_API_SECRET in the .env file. Prompts interactively if no value provided.",
)
config_secret_parser.add_argument("value", nargs="?", help="API secret value (omit to prompt interactively)")
_add_global_flags(config_secret_parser)
account_parser = subparsers.add_parser(
"account", aliases=["acc", "a"], help="List asset balances and notional values",
description="List all non-zero spot balances with free/locked totals, notional USDT value, and dust flag.",
)
_add_global_flags(account_parser)
market_parser = subparsers.add_parser(
"market", aliases=["m"], help="Batch market queries",
description="Query market data: 24h tickers and OHLCV klines for one or more trading pairs.",
)
market_subparsers = market_parser.add_subparsers(dest="market_command")
tickers_parser = market_subparsers.add_parser(
"tickers", aliases=["tk", "t"], help="Fetch ticker statistics",
description="Fetch ticker statistics (last price, change %, volume) for one or more symbols with optional window.",
)
tickers_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query (e.g. BTCUSDT ETH/USDT)")
tickers_parser.add_argument(
"-w", "--window",
choices=["1m", "2m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "2d", "3d", "5d", "7d", "15d", "30d"],
default="1d",
help="Rolling statistics window (default: 1d)",
)
_add_global_flags(tickers_parser)
klines_parser = market_subparsers.add_parser(
"klines", aliases=["k"], help="Fetch OHLCV klines",
description="Fetch OHLCV candlestick data for one or more symbols.",
)
klines_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query")
klines_parser.add_argument(
"-i", "--interval", default="1h",
help="Kline interval: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M (default: 1h)",
)
klines_parser.add_argument("-l", "--limit", type=int, default=100, help="Number of candles (default: 100)")
_add_global_flags(klines_parser)
buy_parser = subparsers.add_parser(
"buy", aliases=["b"], help="Buy base asset",
description="Place a spot BUY order. Market buys require --quote. Limit buys require --qty and --price.",
)
buy_parser.add_argument("symbol", metavar="SYM", help="Trading pair (e.g. BTCUSDT)")
buy_parser.add_argument("-q", "--qty", type=float, help="Base asset quantity (limit orders)")
buy_parser.add_argument("-Q", "--quote", type=float, help="Quote asset amount (market buy only)")
buy_parser.add_argument("-t", "--type", choices=["market", "limit"], default="market", help="Order type (default: market)")
buy_parser.add_argument("-p", "--price", type=float, help="Limit price")
buy_parser.add_argument("-d", "--dry-run", action="store_true", help="Simulate without sending")
_add_global_flags(buy_parser)
sell_parser = subparsers.add_parser(
"sell", aliases=["s"], help="Sell base asset",
description="Place a spot SELL order. Requires --qty. Limit sells also require --price.",
)
sell_parser.add_argument("symbol", metavar="SYM", help="Trading pair (e.g. BTCUSDT)")
sell_parser.add_argument("-q", "--qty", type=float, help="Base asset quantity")
sell_parser.add_argument("-t", "--type", choices=["market", "limit"], default="market", help="Order type (default: market)")
sell_parser.add_argument("-p", "--price", type=float, help="Limit price")
sell_parser.add_argument("-d", "--dry-run", action="store_true", help="Simulate without sending")
_add_global_flags(sell_parser)
portfolio_parser = subparsers.add_parser(
"portfolio", aliases=["pf", "p"], help="Score current holdings",
description="Review current spot holdings and generate add/hold/trim/exit recommendations.",
)
_add_global_flags(portfolio_parser)
opportunity_parser = subparsers.add_parser(
"opportunity", aliases=["opp", "o"], help="Scan market for opportunities",
description="Scan the market for trading opportunities and return the top-N candidates with signals.",
)
opportunity_parser.add_argument("-s", "--symbols", nargs="*", metavar="SYM", help="Restrict scan to specific symbols")
_add_global_flags(opportunity_parser)
opportunity_subparsers = opportunity_parser.add_subparsers(dest="opportunity_command")
scan_parser = opportunity_subparsers.add_parser(
"scan", help="Scan market for opportunities",
description="Scan the market for trading opportunities and return the top-N candidates with signals.",
)
scan_parser.add_argument("-s", "--symbols", nargs="*", metavar="SYM", help="Restrict scan to specific symbols")
_add_global_flags(scan_parser)
dataset_parser = opportunity_subparsers.add_parser(
"dataset", aliases=["ds"], help="Collect historical opportunity evaluation dataset",
description="Collect point-in-time market data for opportunity simulation and evaluation.",
)
dataset_parser.add_argument("-s", "--symbols", nargs="*", metavar="SYM", help="Restrict dataset to symbols")
dataset_parser.add_argument("--simulate-days", type=float, help="Forward simulation/evaluation window in days")
dataset_parser.add_argument("--run-days", type=float, help="Continuous scan simulation window in days")
dataset_parser.add_argument("-o", "--output", help="Output dataset JSON path")
_add_global_flags(dataset_parser)
evaluate_parser = opportunity_subparsers.add_parser(
"evaluate", aliases=["eval", "ev"], help="Evaluate opportunity accuracy from a historical dataset",
description="Run a walk-forward evaluation over an opportunity dataset using point-in-time candles only.",
)
evaluate_parser.add_argument("dataset", help="Path to an opportunity dataset JSON file")
evaluate_parser.add_argument("--horizon-hours", type=float, help="Forward evaluation horizon in hours")
evaluate_parser.add_argument("--take-profit-pct", type=float, help="Trigger success take-profit threshold in percent")
evaluate_parser.add_argument("--stop-loss-pct", type=float, help="Stop-loss threshold in percent")
evaluate_parser.add_argument("--setup-target-pct", type=float, help="Setup success target threshold in percent")
evaluate_parser.add_argument("--lookback", type=int, help="Closed candles used for each point-in-time score")
evaluate_parser.add_argument("--top-n", type=int, help="Evaluate only the top-N ranked symbols at each decision time")
evaluate_parser.add_argument("--examples", type=int, default=20, help="Number of example judgments to include")
_add_global_flags(evaluate_parser)
optimize_parser = opportunity_subparsers.add_parser(
"optimize", aliases=["opt"], help="Optimize opportunity model weights from a historical dataset",
description="Coordinate-search normalized model weights while keeping decision thresholds fixed.",
)
optimize_parser.add_argument("dataset", help="Path to an opportunity dataset JSON file")
optimize_parser.add_argument("--horizon-hours", type=float, help="Forward evaluation horizon in hours")
optimize_parser.add_argument("--take-profit-pct", type=float, help="Trigger success take-profit threshold in percent")
optimize_parser.add_argument("--stop-loss-pct", type=float, help="Stop-loss threshold in percent")
optimize_parser.add_argument("--setup-target-pct", type=float, help="Setup success target threshold in percent")
optimize_parser.add_argument("--lookback", type=int, help="Closed candles used for each point-in-time score")
optimize_parser.add_argument("--top-n", type=int, help="Evaluate only the top-N ranked symbols at each decision time")
optimize_parser.add_argument("--passes", type=int, default=2, help="Coordinate-search passes over model weights")
_add_global_flags(optimize_parser)
strategy_parser = subparsers.add_parser(
"strategy", aliases=["strat", "st"], help="Combined opportunity + portfolio trade signals",
description="Generate unified buy/sell/hold signals by combining opportunity scanning and portfolio analysis.",
)
strategy_parser.add_argument("-s", "--symbols", nargs="*", metavar="SYM", help="Restrict scan to specific symbols")
_add_global_flags(strategy_parser)
backtest_parser = subparsers.add_parser(
"backtest", aliases=["bt"], help="Backtest combined strategy on historical dataset",
description="Run a walk-forward backtest using historical kline datasets with virtual cash and positions.",
)
backtest_parser.add_argument("dataset", help="Path to an opportunity dataset JSON file")
backtest_parser.add_argument("--initial-cash", type=float, help="Initial cash allocation (default: 10000)")
backtest_parser.add_argument("--max-positions", type=int, help="Maximum simultaneous positions (default: 5)")
backtest_parser.add_argument("--position-size-pct", type=float, help="Cash percentage per position (default: 0.2)")
backtest_parser.add_argument("--commission-pct", type=float, help="Commission per trade in percent (default: 0.1)")
backtest_parser.add_argument("--lookback", type=int, help="Closed candles used for each point-in-time score")
backtest_parser.add_argument("--decision-interval", type=int, help="Minimum minutes between decision points (default: 0 = every candle)")
_add_global_flags(backtest_parser)
upgrade_parser = subparsers.add_parser(
"upgrade", help="Upgrade coinhunter to the latest version",
description="Upgrade the coinhunter package using pipx (preferred) or pip.",
)
_add_global_flags(upgrade_parser)
catlog_parser = subparsers.add_parser(
"catlog", help="Read recent audit log entries",
description="Read recent audit log entries across all days with optional limit and offset pagination.",
)
catlog_parser.add_argument("-n", "--limit", type=int, default=10, help="Number of entries (default: 10)")
catlog_parser.add_argument(
"-o", "--offset", type=int, default=0, help="Skip the most recent N entries (default: 0)"
)
catlog_parser.add_argument("-d", "--dry-run", action="store_true", help="Read dry-run audit logs")
_add_global_flags(catlog_parser)
completion_parser = subparsers.add_parser(
"completion", help="Generate shell completion script",
description="Generate a shell completion script for bash or zsh.",
)
completion_parser.add_argument("shell", choices=["bash", "zsh"], help="Target shell")
_add_global_flags(completion_parser)
return parser
_CANONICAL_COMMANDS = {
"b": "buy",
"s": "sell",
"acc": "account",
"a": "account",
"m": "market",
"pf": "portfolio",
"p": "portfolio",
"opp": "opportunity",
"o": "opportunity",
"cfg": "config",
"c": "config",
"strat": "strategy",
"st": "strategy",
"bt": "backtest",
}
_CANONICAL_SUBCOMMANDS = {
"tk": "tickers",
"t": "tickers",
"k": "klines",
"ds": "dataset",
"eval": "evaluate",
"ev": "evaluate",
"opt": "optimize",
}
_COMMANDS_WITH_SUBCOMMANDS = {"market", "config", "opportunity"}
def _get_doc_key(argv: list[str]) -> str | None:
"""Infer command/subcommand from argv for --doc lookup."""
tokens = [a for a in argv if a != "--doc" and not a.startswith("-")]
if not tokens:
return None
cmd = _CANONICAL_COMMANDS.get(tokens[0], tokens[0])
if cmd in _COMMANDS_WITH_SUBCOMMANDS and len(tokens) > 1:
sub = _CANONICAL_SUBCOMMANDS.get(tokens[1], tokens[1])
sub_key = f"{cmd}/{sub}"
if sub_key in COMMAND_DOCS:
return sub_key
return cmd
def _reorder_flag(argv: list[str], flag: str, short_flag: str | None = None) -> list[str]:
"""Move a global flag from after subcommands to before them so argparse can parse it."""
flags = {flag}
if short_flag:
flags.add(short_flag)
subcommand_idx: int | None = None
for i, arg in enumerate(argv):
if not arg.startswith("-"):
subcommand_idx = i
break
if subcommand_idx is None:
return argv
new_argv: list[str] = []
present = False
for i, arg in enumerate(argv):
if i >= subcommand_idx and arg in flags:
present = True
continue
new_argv.append(arg)
if present:
new_argv.insert(subcommand_idx, flag)
return new_argv
def main(argv: list[str] | None = None) -> int:
raw_argv = argv if argv is not None else sys.argv[1:]
if "--doc" in raw_argv:
doc_key = _get_doc_key(raw_argv)
if doc_key is None:
print("Available docs: " + ", ".join(sorted(COMMAND_DOCS.keys())))
return 0
entry = COMMAND_DOCS.get(doc_key)
if entry is None:
print(f"No documentation available for {doc_key}.")
return 0
is_agent = "--agent" in raw_argv or "-a" in raw_argv
doc = entry.get("json" if is_agent else "tui", entry.get("tui", ""))
print(doc)
return 0
parser = build_parser()
raw_argv = _reorder_flag(raw_argv, "--agent", "-a")
args = parser.parse_args(raw_argv)
args.agent = bool(getattr(args, "agent", False) or "--agent" in raw_argv or "-a" in raw_argv)
# Normalize aliases to canonical command names
if args.command:
args.command = _CANONICAL_COMMANDS.get(args.command, args.command)
for attr in ("account_command", "market_command", "config_command", "opportunity_command"):
val = getattr(args, attr, None)
if val:
setattr(args, attr, _CANONICAL_SUBCOMMANDS.get(val, val))
try:
if not args.command:
parser.print_help()
return 0
if args.command == "init":
paths = get_runtime_paths()
init_result = ensure_init_files(paths, force=args.force)
init_result["completion"] = install_shell_completion(parser)
# Interactive prompt for API key/secret if missing and tty available
if not args.no_prompt and sys.stdin.isatty():
from .config import load_env_file
env_data = load_env_file(paths)
api_key = env_data.get("BINANCE_API_KEY", "").strip()
api_secret = env_data.get("BINANCE_API_SECRET", "").strip()
if not api_key:
key_input = input("Binance API Key: ").strip()
if key_input:
set_env_value(paths, "BINANCE_API_KEY", key_input)
init_result["api_key_set"] = True
if not api_secret:
import getpass
secret_input = getpass.getpass("Binance API Secret: ").strip()
if secret_input:
set_env_value(paths, "BINANCE_API_SECRET", secret_input)
init_result["api_secret_set"] = True
print_output(init_result, agent=args.agent)
return 0
if args.command == "config":
paths = get_runtime_paths()
if args.config_command == "get":
if not args.key:
# List all config keys
config = load_config()
print_output(config, agent=args.agent)
return 0
config = load_config()
value = get_config_value(config, args.key)
if value is None:
print(f"error: key '{args.key}' not found in config", file=sys.stderr)
return 1
print_output({args.key: value}, agent=args.agent)
return 0
if args.config_command == "set":
set_config_value(paths.config_file, args.key, args.value)
print_output({"key": args.key, "value": args.value, "status": "updated"}, agent=args.agent)
return 0
if args.config_command == "key":
value = args.value
if not value and sys.stdin.isatty():
value = input("Binance API Key: ").strip()
if not value:
print("error: API key is required", file=sys.stderr)
return 1
set_env_value(paths, "BINANCE_API_KEY", value)
print_output({"key": "BINANCE_API_KEY", "status": "updated"}, agent=args.agent)
return 0
if args.config_command == "secret":
value = args.value
if not value and sys.stdin.isatty():
import getpass
value = getpass.getpass("Binance API Secret: ").strip()
if not value:
print("error: API secret is required", file=sys.stderr)
return 1
set_env_value(paths, "BINANCE_API_SECRET", value)
print_output({"key": "BINANCE_API_SECRET", "status": "updated"}, agent=args.agent)
return 0
parser.error("config requires one of: get, set, key, secret")
if args.command == "completion":
import shtab
print(shtab.complete(parser, shell=args.shell, preamble=""))
return 0
if args.command == "upgrade":
with with_spinner("Upgrading coinhunter...", enabled=not args.agent):
result = self_upgrade()
print_output(result, agent=args.agent)
return 0
if args.command == "catlog":
with with_spinner("Reading audit logs...", enabled=not args.agent):
entries = read_audit_log(limit=args.limit, offset=args.offset, dry_run=args.dry_run)
print_output(
{"entries": entries, "limit": args.limit, "offset": args.offset, "total": len(entries), "dry_run": args.dry_run},
agent=args.agent,
)
return 0
config = load_config()
if args.command == "account":
spot_client = _load_spot_client(config)
with with_spinner("Fetching balances...", enabled=not args.agent):
result = account_service.get_balances(config, spot_client=spot_client)
print_output(result, agent=args.agent)
return 0
if args.command == "market":
spot_client = _load_spot_client(config)
if args.market_command == "tickers":
with with_spinner("Fetching tickers...", enabled=not args.agent):
result = market_service.get_tickers(
config, args.symbols, spot_client=spot_client, window=args.window
)
print_output(result, agent=args.agent)
return 0
if args.market_command == "klines":
with with_spinner("Fetching klines...", enabled=not args.agent):
result = market_service.get_klines(
config,
args.symbols,
interval=args.interval,
limit=args.limit,
spot_client=spot_client,
)
print_output(result, agent=args.agent)
return 0
parser.error("market requires one of: tickers, klines")
if args.command == "buy":
spot_client = _load_spot_client(config)
with with_spinner("Placing order...", enabled=not args.agent):
result = trade_service.execute_spot_trade(
config,
side="buy",
symbol=args.symbol,
qty=args.qty,
quote=args.quote,
order_type=args.type,
price=args.price,
dry_run=True if args.dry_run else None,
spot_client=spot_client,
)
print_output(result, agent=args.agent)
return 0
if args.command == "sell":
spot_client = _load_spot_client(config)
with with_spinner("Placing order...", enabled=not args.agent):
result = trade_service.execute_spot_trade(
config,
side="sell",
symbol=args.symbol,
qty=args.qty,
quote=None,
order_type=args.type,
price=args.price,
dry_run=True if args.dry_run else None,
spot_client=spot_client,
)
print_output(result, agent=args.agent)
return 0
if args.command == "portfolio":
spot_client = _load_spot_client(config)
with with_spinner("Analyzing portfolio...", enabled=not args.agent):
result = portfolio_service.analyze_portfolio(config, spot_client=spot_client)
print_output(result, agent=args.agent)
return 0
if args.command == "strategy":
spot_client = _load_spot_client(config)
with with_spinner("Generating trade signals...", enabled=not args.agent):
result = strategy_service.generate_trade_signals(
config, spot_client=spot_client, symbols=args.symbols
)
print_output(result, agent=args.agent)
return 0
if args.command == "backtest":
with with_spinner("Running backtest...", enabled=not args.agent):
result = backtest_service.run_backtest(
config,
dataset_path=args.dataset,
initial_cash=args.initial_cash,
max_positions=args.max_positions,
position_size_pct=args.position_size_pct / 100.0 if args.position_size_pct is not None else None,
commission_pct=args.commission_pct / 100.0 if args.commission_pct is not None else None,
lookback=args.lookback,
decision_interval_minutes=args.decision_interval,
)
print_output(result, agent=args.agent)
return 0
if args.command == "opportunity":
if args.opportunity_command == "optimize":
with with_spinner("Optimizing opportunity model...", enabled=not args.agent):
result = opportunity_evaluation_service.optimize_opportunity_model(
config,
dataset_path=args.dataset,
horizon_hours=args.horizon_hours,
take_profit=args.take_profit_pct / 100.0 if args.take_profit_pct is not None else None,
stop_loss=args.stop_loss_pct / 100.0 if args.stop_loss_pct is not None else None,
setup_target=args.setup_target_pct / 100.0 if args.setup_target_pct is not None else None,
lookback=args.lookback,
top_n=args.top_n,
passes=args.passes,
)
print_output(result, agent=args.agent)
return 0
if args.opportunity_command == "evaluate":
with with_spinner("Evaluating opportunity dataset...", enabled=not args.agent):
result = opportunity_evaluation_service.evaluate_opportunity_dataset(
config,
dataset_path=args.dataset,
horizon_hours=args.horizon_hours,
take_profit=args.take_profit_pct / 100.0 if args.take_profit_pct is not None else None,
stop_loss=args.stop_loss_pct / 100.0 if args.stop_loss_pct is not None else None,
setup_target=args.setup_target_pct / 100.0 if args.setup_target_pct is not None else None,
lookback=args.lookback,
top_n=args.top_n,
max_examples=args.examples,
)
print_output(result, agent=args.agent)
return 0
if args.opportunity_command == "dataset":
with with_spinner("Collecting opportunity dataset...", enabled=not args.agent):
result = opportunity_dataset_service.collect_opportunity_dataset(
config,
symbols=args.symbols,
simulate_days=args.simulate_days,
run_days=args.run_days,
output_path=args.output,
)
print_output(result, agent=args.agent)
return 0
spot_client = _load_spot_client(config)
with with_spinner("Scanning opportunities...", enabled=not args.agent):
result = opportunity_service.scan_opportunities(
config, spot_client=spot_client, symbols=args.symbols
)
print_output(result, agent=args.agent)
return 0
parser.error(f"Unsupported command {args.command}")
return 2
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1