Compare commits
1 Commits
d629c25232
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3855477155 |
@@ -85,11 +85,9 @@ coin market klines --doc
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Account (aliases: a, acc)
|
# Account (aliases: a, acc)
|
||||||
coinhunter account overview
|
coinhunter account
|
||||||
coinhunter account overview --agent
|
coinhunter account --agent
|
||||||
coin a ov
|
coin a
|
||||||
coin acc bal
|
|
||||||
coin a pos
|
|
||||||
|
|
||||||
# Market (aliases: m)
|
# Market (aliases: m)
|
||||||
coinhunter market tickers BTCUSDT ETH/USDT sol-usdt
|
coinhunter market tickers BTCUSDT ETH/USDT sol-usdt
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from requests.exceptions import ( # type: ignore[import-untyped]
|
from requests.exceptions import (
|
||||||
RequestException,
|
RequestException,
|
||||||
SSLError,
|
SSLError,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from .services import (
|
|||||||
EPILOG = """\
|
EPILOG = """\
|
||||||
examples:
|
examples:
|
||||||
coin init
|
coin init
|
||||||
coin acc ov
|
coin account
|
||||||
coin m tk BTCUSDT ETHUSDT
|
coin m tk BTCUSDT ETHUSDT
|
||||||
coin m k BTCUSDT -i 1h -l 50
|
coin m k BTCUSDT -i 1h -l 50
|
||||||
coin buy BTCUSDT -Q 100 -d
|
coin buy BTCUSDT -Q 100 -d
|
||||||
@@ -49,38 +49,20 @@ Fields:
|
|||||||
files_created – list of generated files
|
files_created – list of generated files
|
||||||
completion – shell completion installation status
|
completion – shell completion installation status
|
||||||
""",
|
""",
|
||||||
"account/overview": """\
|
"account": """\
|
||||||
Output: JSON
|
Output: JSON
|
||||||
{
|
{
|
||||||
"total_btc": 1.234,
|
"balances": [
|
||||||
"total_usdt": 50000.0,
|
{"asset": "BTC", "free": 0.5, "locked": 0.1, "total": 0.6, "notional_usdt": 30000.0, "is_dust": false}
|
||||||
"assets": [{"asset": "BTC", "free": 0.5, "locked": 0.1}]
|
]
|
||||||
}
|
}
|
||||||
Fields:
|
|
||||||
total_btc – total equity denominated in BTC
|
|
||||||
total_usdt – total equity denominated in USDT
|
|
||||||
assets – list of non-zero balances
|
|
||||||
""",
|
|
||||||
"account/balances": """\
|
|
||||||
Output: JSON array
|
|
||||||
[
|
|
||||||
{"asset": "BTC", "free": 0.5, "locked": 0.1, "total": 0.6}
|
|
||||||
]
|
|
||||||
Fields:
|
Fields:
|
||||||
asset – asset symbol
|
asset – asset symbol
|
||||||
free – available balance
|
free – available balance
|
||||||
locked – frozen/locked balance
|
locked – frozen/locked balance
|
||||||
total – free + locked
|
total – free + locked
|
||||||
""",
|
notional_usdt – estimated value in USDT
|
||||||
"account/positions": """\
|
is_dust – true if value is below dust threshold
|
||||||
Output: JSON array
|
|
||||||
[
|
|
||||||
{"symbol": "BTCUSDT", "positionAmt": 0.01, "entryPrice": 90000.0}
|
|
||||||
]
|
|
||||||
Fields:
|
|
||||||
symbol – trading pair
|
|
||||||
positionAmt – quantity held (positive long, negative short)
|
|
||||||
entryPrice – average entry price
|
|
||||||
""",
|
""",
|
||||||
"market/tickers": """\
|
"market/tickers": """\
|
||||||
Output: JSON object keyed by normalized symbol
|
Output: JSON object keyed by normalized symbol
|
||||||
@@ -214,7 +196,7 @@ def _load_spot_client(config: dict[str, Any], *, client: Any | None = None) -> S
|
|||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="coinhunter",
|
prog="coinhunter",
|
||||||
description="CoinHunter V2 Binance-first trading CLI",
|
description="Binance-first trading CLI for account balances, market data, trade execution, and opportunity scanning.",
|
||||||
epilog=EPILOG,
|
epilog=EPILOG,
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
@@ -223,64 +205,105 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
parser.add_argument("--doc", action="store_true", help="Show output schema and field descriptions for the command")
|
parser.add_argument("--doc", action="store_true", help="Show output schema and field descriptions for the command")
|
||||||
subparsers = parser.add_subparsers(dest="command")
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
init_parser = subparsers.add_parser("init", help="Generate config.toml, .env, and log directory")
|
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.",
|
||||||
|
)
|
||||||
init_parser.add_argument("-f", "--force", action="store_true", help="Overwrite existing files")
|
init_parser.add_argument("-f", "--force", action="store_true", help="Overwrite existing files")
|
||||||
|
_add_global_flags(init_parser)
|
||||||
|
|
||||||
account_parser = subparsers.add_parser("account", aliases=["acc", "a"], help="Account overview, balances, and positions")
|
account_parser = subparsers.add_parser(
|
||||||
account_subparsers = account_parser.add_subparsers(dest="account_command")
|
"account", aliases=["acc", "a"], help="List asset balances and notional values",
|
||||||
account_commands_help = {
|
description="List all non-zero spot balances with free/locked totals, notional USDT value, and dust flag.",
|
||||||
"overview": "Total equity and summary",
|
)
|
||||||
"balances": "List asset balances",
|
_add_global_flags(account_parser)
|
||||||
"positions": "List open positions",
|
|
||||||
}
|
|
||||||
account_aliases = {
|
|
||||||
"overview": ["ov"],
|
|
||||||
"balances": ["bal", "b"],
|
|
||||||
"positions": ["pos", "p"],
|
|
||||||
}
|
|
||||||
for name in ("overview", "balances", "positions"):
|
|
||||||
account_subparsers.add_parser(name, aliases=account_aliases[name], help=account_commands_help[name])
|
|
||||||
|
|
||||||
market_parser = subparsers.add_parser("market", aliases=["m"], help="Batch market queries")
|
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")
|
market_subparsers = market_parser.add_subparsers(dest="market_command")
|
||||||
tickers_parser = market_subparsers.add_parser("tickers", aliases=["tk", "t"], help="Fetch 24h ticker data")
|
tickers_parser = market_subparsers.add_parser(
|
||||||
|
"tickers", aliases=["tk", "t"], help="Fetch 24h ticker data",
|
||||||
|
description="Fetch 24h ticker statistics (last price, change %, volume) for one or more symbols.",
|
||||||
|
)
|
||||||
tickers_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query (e.g. BTCUSDT ETH/USDT)")
|
tickers_parser.add_argument("symbols", nargs="+", metavar="SYM", help="Symbols to query (e.g. BTCUSDT ETH/USDT)")
|
||||||
klines_parser = market_subparsers.add_parser("klines", aliases=["k"], help="Fetch OHLCV klines")
|
_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("symbols", nargs="+", metavar="SYM", help="Symbols to query")
|
||||||
klines_parser.add_argument("-i", "--interval", default="1h", help="Kline interval (default: 1h)")
|
klines_parser.add_argument("-i", "--interval", default="1h", help="Kline interval (default: 1h)")
|
||||||
klines_parser.add_argument("-l", "--limit", type=int, default=100, help="Number of candles (default: 100)")
|
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")
|
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("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", "--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("-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("-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("-p", "--price", type=float, help="Limit price")
|
||||||
buy_parser.add_argument("-d", "--dry-run", action="store_true", help="Simulate without sending")
|
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")
|
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("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("-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("-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("-p", "--price", type=float, help="Limit price")
|
||||||
sell_parser.add_argument("-d", "--dry-run", action="store_true", help="Simulate without sending")
|
sell_parser.add_argument("-d", "--dry-run", action="store_true", help="Simulate without sending")
|
||||||
|
_add_global_flags(sell_parser)
|
||||||
|
|
||||||
opportunity_parser = subparsers.add_parser("opportunity", aliases=["opp", "o"], help="Portfolio analysis and market scanning")
|
opportunity_parser = subparsers.add_parser(
|
||||||
|
"opportunity", aliases=["opp", "o"], help="Portfolio analysis and market scanning",
|
||||||
|
description="Analyze your portfolio and scan the market for trading opportunities.",
|
||||||
|
)
|
||||||
opportunity_subparsers = opportunity_parser.add_subparsers(dest="opportunity_command")
|
opportunity_subparsers = opportunity_parser.add_subparsers(dest="opportunity_command")
|
||||||
opportunity_subparsers.add_parser("portfolio", aliases=["pf", "p"], help="Score current holdings")
|
portfolio_parser = opportunity_subparsers.add_parser(
|
||||||
scan_parser = opportunity_subparsers.add_parser("scan", help="Scan market for opportunities")
|
"portfolio", aliases=["pf", "p"], help="Score current holdings",
|
||||||
|
description="Score your current spot holdings and generate add/hold/trim/exit recommendations.",
|
||||||
|
)
|
||||||
|
_add_global_flags(portfolio_parser)
|
||||||
|
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")
|
scan_parser.add_argument("-s", "--symbols", nargs="*", metavar="SYM", help="Restrict scan to specific symbols")
|
||||||
|
_add_global_flags(scan_parser)
|
||||||
|
|
||||||
subparsers.add_parser("upgrade", help="Upgrade coinhunter to the latest version")
|
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")
|
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("-n", "--limit", type=int, default=10, help="Number of entries (default: 10)")
|
||||||
catlog_parser.add_argument(
|
catlog_parser.add_argument(
|
||||||
"-o", "--offset", type=int, default=0, help="Skip the most recent N entries (default: 0)"
|
"-o", "--offset", type=int, default=0, help="Skip the most recent N entries (default: 0)"
|
||||||
)
|
)
|
||||||
|
_add_global_flags(catlog_parser)
|
||||||
|
|
||||||
completion_parser = subparsers.add_parser("completion", help="Generate shell completion script")
|
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")
|
completion_parser.add_argument("shell", choices=["bash", "zsh"], help="Target shell")
|
||||||
|
_add_global_flags(completion_parser)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@@ -296,18 +319,13 @@ _CANONICAL_COMMANDS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_CANONICAL_SUBCOMMANDS = {
|
_CANONICAL_SUBCOMMANDS = {
|
||||||
"ov": "overview",
|
|
||||||
"bal": "balances",
|
|
||||||
"b": "balances",
|
|
||||||
"pos": "positions",
|
|
||||||
"p": "positions",
|
|
||||||
"tk": "tickers",
|
"tk": "tickers",
|
||||||
"t": "tickers",
|
"t": "tickers",
|
||||||
"k": "klines",
|
"k": "klines",
|
||||||
"pf": "portfolio",
|
"pf": "portfolio",
|
||||||
}
|
}
|
||||||
|
|
||||||
_COMMANDS_WITH_SUBCOMMANDS = {"account", "market", "opportunity"}
|
_COMMANDS_WITH_SUBCOMMANDS = {"market", "opportunity"}
|
||||||
|
|
||||||
|
|
||||||
def _get_doc_key(argv: list[str]) -> str | None:
|
def _get_doc_key(argv: list[str]) -> str | None:
|
||||||
@@ -391,22 +409,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
|
|
||||||
if args.command == "account":
|
if args.command == "account":
|
||||||
spot_client = _load_spot_client(config)
|
spot_client = _load_spot_client(config)
|
||||||
if args.account_command == "overview":
|
|
||||||
with with_spinner("Fetching account overview...", enabled=not args.agent):
|
|
||||||
result = account_service.get_overview(config, spot_client=spot_client)
|
|
||||||
print_output(result, agent=args.agent)
|
|
||||||
return 0
|
|
||||||
if args.account_command == "balances":
|
|
||||||
with with_spinner("Fetching balances...", enabled=not args.agent):
|
with with_spinner("Fetching balances...", enabled=not args.agent):
|
||||||
result = account_service.get_balances(config, spot_client=spot_client)
|
result = account_service.get_balances(config, spot_client=spot_client)
|
||||||
print_output(result, agent=args.agent)
|
print_output(result, agent=args.agent)
|
||||||
return 0
|
return 0
|
||||||
if args.account_command == "positions":
|
|
||||||
with with_spinner("Fetching positions...", enabled=not args.agent):
|
|
||||||
result = account_service.get_positions(config, spot_client=spot_client)
|
|
||||||
print_output(result, agent=args.agent)
|
|
||||||
return 0
|
|
||||||
parser.error("account requires one of: overview, balances, positions")
|
|
||||||
|
|
||||||
if args.command == "market":
|
if args.command == "market":
|
||||||
spot_client = _load_spot_client(config)
|
spot_client = _load_spot_client(config)
|
||||||
|
|||||||
@@ -213,39 +213,27 @@ def _render_tui(payload: Any) -> None:
|
|||||||
print(str(payload))
|
print(str(payload))
|
||||||
return
|
return
|
||||||
|
|
||||||
if "overview" in payload:
|
|
||||||
overview = payload.get("overview", {})
|
|
||||||
print(f"\n{_BOLD}{_CYAN} ACCOUNT OVERVIEW {_RESET}")
|
|
||||||
print(f" Total Equity: {_GREEN}{_fmt_number(overview.get('total_equity_usdt', 0))} USDT{_RESET}")
|
|
||||||
print(f" Spot Assets: {_fmt_number(overview.get('spot_asset_count', 0))}")
|
|
||||||
print(f" Positions: {_fmt_number(overview.get('spot_position_count', 0))}")
|
|
||||||
if payload.get("balances"):
|
|
||||||
print()
|
|
||||||
_render_tui({"balances": payload["balances"]})
|
|
||||||
if payload.get("positions"):
|
|
||||||
print()
|
|
||||||
_render_tui({"positions": payload["positions"]})
|
|
||||||
return
|
|
||||||
|
|
||||||
if "balances" in payload:
|
if "balances" in payload:
|
||||||
rows = payload["balances"]
|
rows = payload["balances"]
|
||||||
table_rows: list[list[str]] = []
|
table_rows: list[list[str]] = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
|
is_dust = r.get("is_dust", False)
|
||||||
|
dust_label = f"{_DIM}dust{_RESET}" if is_dust else ""
|
||||||
table_rows.append(
|
table_rows.append(
|
||||||
[
|
[
|
||||||
r.get("market_type", ""),
|
|
||||||
r.get("asset", ""),
|
r.get("asset", ""),
|
||||||
_fmt_number(r.get("free", 0)),
|
_fmt_number(r.get("free", 0)),
|
||||||
_fmt_number(r.get("locked", 0)),
|
_fmt_number(r.get("locked", 0)),
|
||||||
_fmt_number(r.get("total", 0)),
|
_fmt_number(r.get("total", 0)),
|
||||||
_fmt_number(r.get("notional_usdt", 0)),
|
_fmt_number(r.get("notional_usdt", 0)),
|
||||||
|
dust_label,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
_print_box_table(
|
_print_box_table(
|
||||||
"BALANCES",
|
"BALANCES",
|
||||||
["Market", "Asset", "Free", "Locked", "Total", "Notional (USDT)"],
|
["Asset", "Free", "Locked", "Total", "Notional (USDT)", ""],
|
||||||
table_rows,
|
table_rows,
|
||||||
aligns=["left", "left", "right", "right", "right", "right"],
|
aligns=["left", "right", "right", "right", "right", "left"],
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class AssetBalance:
|
|||||||
locked: float
|
locked: float
|
||||||
total: float
|
total: float
|
||||||
notional_usdt: float
|
notional_usdt: float
|
||||||
|
is_dust: bool
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -59,6 +60,7 @@ def get_balances(
|
|||||||
spot_client: Any,
|
spot_client: Any,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
quote = str(config.get("market", {}).get("default_quote", "USDT")).upper()
|
quote = str(config.get("market", {}).get("default_quote", "USDT")).upper()
|
||||||
|
dust = float(config.get("trading", {}).get("dust_usdt_threshold", 0.0))
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
balances, _, price_map = _spot_account_data(spot_client, quote)
|
balances, _, price_map = _spot_account_data(spot_client, quote)
|
||||||
for item in balances:
|
for item in balances:
|
||||||
@@ -68,6 +70,7 @@ def get_balances(
|
|||||||
if total <= 0:
|
if total <= 0:
|
||||||
continue
|
continue
|
||||||
asset = item["asset"]
|
asset = item["asset"]
|
||||||
|
notional = total * price_map.get(asset, 0.0)
|
||||||
rows.append(
|
rows.append(
|
||||||
asdict(
|
asdict(
|
||||||
AssetBalance(
|
AssetBalance(
|
||||||
@@ -75,7 +78,8 @@ def get_balances(
|
|||||||
free=free,
|
free=free,
|
||||||
locked=locked,
|
locked=locked,
|
||||||
total=total,
|
total=total,
|
||||||
notional_usdt=total * price_map.get(asset, 0.0),
|
notional_usdt=notional,
|
||||||
|
is_dust=notional < dust,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -113,60 +117,3 @@ def get_positions(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return {"positions": rows}
|
return {"positions": rows}
|
||||||
|
|
||||||
|
|
||||||
def get_overview(
|
|
||||||
config: dict[str, Any],
|
|
||||||
*,
|
|
||||||
spot_client: Any,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
quote = str(config.get("market", {}).get("default_quote", "USDT")).upper()
|
|
||||||
dust = float(config.get("trading", {}).get("dust_usdt_threshold", 0.0))
|
|
||||||
balances: list[dict[str, Any]] = []
|
|
||||||
positions: list[dict[str, Any]] = []
|
|
||||||
|
|
||||||
spot_balances, _, price_map = _spot_account_data(spot_client, quote)
|
|
||||||
for item in spot_balances:
|
|
||||||
free = float(item.get("free", 0.0))
|
|
||||||
locked = float(item.get("locked", 0.0))
|
|
||||||
total = free + locked
|
|
||||||
if total <= 0:
|
|
||||||
continue
|
|
||||||
asset = item["asset"]
|
|
||||||
balances.append(
|
|
||||||
asdict(
|
|
||||||
AssetBalance(
|
|
||||||
asset=asset,
|
|
||||||
free=free,
|
|
||||||
locked=locked,
|
|
||||||
total=total,
|
|
||||||
notional_usdt=total * price_map.get(asset, 0.0),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
mark_price = price_map.get(asset, 1.0 if asset == quote else 0.0)
|
|
||||||
notional = total * mark_price
|
|
||||||
if notional >= dust:
|
|
||||||
positions.append(
|
|
||||||
asdict(
|
|
||||||
PositionView(
|
|
||||||
symbol=quote if asset == quote else f"{asset}{quote}",
|
|
||||||
quantity=total,
|
|
||||||
entry_price=None,
|
|
||||||
mark_price=mark_price,
|
|
||||||
notional_usdt=notional,
|
|
||||||
side="LONG",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
spot_equity = sum(item["notional_usdt"] for item in balances)
|
|
||||||
overview = asdict(
|
|
||||||
AccountOverview(
|
|
||||||
total_equity_usdt=spot_equity,
|
|
||||||
spot_equity_usdt=spot_equity,
|
|
||||||
spot_asset_count=len(balances),
|
|
||||||
spot_position_count=len(positions),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return {"overview": overview, "balances": balances, "positions": positions}
|
|
||||||
|
|||||||
@@ -69,19 +69,19 @@ class FakeSpotClient:
|
|||||||
|
|
||||||
|
|
||||||
class AccountMarketServicesTestCase(unittest.TestCase):
|
class AccountMarketServicesTestCase(unittest.TestCase):
|
||||||
def test_account_overview_and_dust_filter(self):
|
def test_get_balances_with_dust_flag(self):
|
||||||
config = {
|
config = {
|
||||||
"market": {"default_quote": "USDT"},
|
"market": {"default_quote": "USDT"},
|
||||||
"trading": {"dust_usdt_threshold": 10.0},
|
"trading": {"dust_usdt_threshold": 10.0},
|
||||||
}
|
}
|
||||||
payload = account_service.get_overview(
|
payload = account_service.get_balances(
|
||||||
config,
|
config,
|
||||||
spot_client=FakeSpotClient(),
|
spot_client=FakeSpotClient(),
|
||||||
)
|
)
|
||||||
self.assertEqual(payload["overview"]["total_equity_usdt"], 720.1)
|
balances = {item["asset"]: item for item in payload["balances"]}
|
||||||
symbols = {item["symbol"] for item in payload["positions"]}
|
self.assertFalse(balances["USDT"]["is_dust"])
|
||||||
self.assertNotIn("DOGEUSDT", symbols)
|
self.assertFalse(balances["BTC"]["is_dust"])
|
||||||
self.assertIn("BTCUSDT", symbols)
|
self.assertTrue(balances["DOGE"]["is_dust"])
|
||||||
|
|
||||||
def test_market_tickers_and_scan_universe(self):
|
def test_market_tickers_and_scan_universe(self):
|
||||||
config = {
|
config = {
|
||||||
|
|||||||
@@ -91,6 +91,25 @@ class CLITestCase(unittest.TestCase):
|
|||||||
self.assertIn("lastPrice", output)
|
self.assertIn("lastPrice", output)
|
||||||
self.assertIn("BTCUSDT", output)
|
self.assertIn("BTCUSDT", output)
|
||||||
|
|
||||||
|
def test_account_dispatches(self):
|
||||||
|
captured = {}
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
cli, "load_config", return_value={"binance": {"spot_base_url": "https://test", "recv_window": 5000}, "market": {"default_quote": "USDT"}, "trading": {"dust_usdt_threshold": 10.0}}
|
||||||
|
),
|
||||||
|
patch.object(cli, "get_binance_credentials", return_value={"api_key": "k", "api_secret": "s"}),
|
||||||
|
patch.object(cli, "SpotBinanceClient"),
|
||||||
|
patch.object(
|
||||||
|
cli.account_service, "get_balances", return_value={"balances": [{"asset": "BTC", "is_dust": False}]}
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = cli.main(["account"])
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
self.assertEqual(captured["payload"]["balances"][0]["asset"], "BTC")
|
||||||
|
|
||||||
def test_upgrade_dispatches(self):
|
def test_upgrade_dispatches(self):
|
||||||
captured = {}
|
captured = {}
|
||||||
with (
|
with (
|
||||||
|
|||||||
Reference in New Issue
Block a user