Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions cli/commands/money.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""hl money — withdraw, transfer, deposit, and bridge funds."""
from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Optional

import typer

money_app = typer.Typer(no_args_is_help=True)


def _ensure_path() -> None:
project_root = str(Path(__file__).resolve().parent.parent.parent)
if project_root not in sys.path:
sys.path.insert(0, project_root)


def _confirm(prompt: str, yes: bool) -> None:
if yes:
return
if sys.stdin.isatty():
if not typer.confirm(prompt):
raise typer.Exit()
else:
typer.echo("Refusing to move funds without --yes in non-interactive mode.", err=True)
raise typer.Exit(1)


def _refuse_non_interactive_without_yes(yes: bool) -> None:
"""Fail before expensive or pairing-dependent setup in automation."""
if yes or sys.stdin.isatty():
return
typer.echo("Refusing to move funds without --yes in non-interactive mode.", err=True)
raise typer.Exit(1)


def _submit(request, mainnet: bool) -> None:
from cli.hl_actions import sign_and_submit

result = sign_and_submit(request, mainnet)
typer.echo(json.dumps(result, indent=2))


@money_app.command("withdraw", help="Withdraw USDC from Hyperliquid to Arbitrum")
def withdraw(
amount: str = typer.Argument(..., help="USDC amount"),
destination: str = typer.Argument(..., help="Arbitrum destination address"),
mainnet: bool = typer.Option(False, "--mainnet"),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
) -> None:
_ensure_path()
from cli.hl_actions import build_withdraw

request = build_withdraw(amount, destination, mainnet)
typer.echo(request.summary)
_confirm("Submit this Hyperliquid withdrawal via your paired master wallet?", yes)
_submit(request, mainnet)


@money_app.command("transfer", help="Transfer funds within Hyperliquid")
def transfer(
kind: str = typer.Argument(..., help="usd, spot, perp-spot, vault, subaccount, subaccount-spot, send-asset"),
amount: str = typer.Argument(..., help="Amount to transfer"),
destination: Optional[str] = typer.Argument(None, help="Destination address/vault/sub-account when needed"),
token: str = typer.Option("USDC", "--token", help="Spot token symbol for spot transfers"),
to_perp: bool = typer.Option(False, "--to-perp", help="For perp-spot transfers, move spot USDC to perps"),
to_spot: bool = typer.Option(False, "--to-spot", help="For perp-spot transfers, move perp USDC to spot"),
deposit: bool = typer.Option(False, "--deposit", help="For vault/sub-account transfers, deposit into target"),
withdraw_from_target: bool = typer.Option(
False,
"--withdraw-from-target",
help="For vault/sub-account transfers, withdraw from target",
),
source_dex: str = typer.Option("", "--source-dex", help="For send-asset, source dex; empty string is perp"),
destination_dex: str = typer.Option("", "--destination-dex", help="For send-asset, destination dex"),
mainnet: bool = typer.Option(False, "--mainnet"),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
) -> None:
_ensure_path()
from cli.hl_actions import (
build_send_asset,
build_spot_transfer,
build_sub_account_spot_transfer,
build_sub_account_transfer,
build_usd_class_transfer,
build_usd_transfer,
build_vault_transfer,
parse_whole_usd,
)

kind = kind.lower()
if kind == "usd":
if destination is None:
raise typer.BadParameter("usd transfers require a destination address")
request = build_usd_transfer(amount, destination, mainnet)
elif kind == "spot":
if destination is None:
raise typer.BadParameter("spot transfers require a destination address")
request = build_spot_transfer(amount, destination, token, mainnet)
elif kind == "perp-spot":
if to_perp == to_spot:
raise typer.BadParameter("Choose exactly one of --to-perp or --to-spot")
request = build_usd_class_transfer(amount, to_perp=to_perp, mainnet=mainnet)
elif kind == "vault":
if destination is None:
raise typer.BadParameter("vault transfers require a vault address")
if deposit == withdraw_from_target:
raise typer.BadParameter("Choose exactly one of --deposit or --withdraw-from-target")
request = build_vault_transfer(destination, deposit, parse_whole_usd(amount), mainnet)
elif kind == "subaccount":
if destination is None:
raise typer.BadParameter("subaccount transfers require a sub-account user address")
if deposit == withdraw_from_target:
raise typer.BadParameter("Choose exactly one of --deposit or --withdraw-from-target")
request = build_sub_account_transfer(destination, deposit, parse_whole_usd(amount), mainnet)
elif kind == "subaccount-spot":
if destination is None:
raise typer.BadParameter("subaccount-spot transfers require a sub-account user address")
if deposit == withdraw_from_target:
raise typer.BadParameter("Choose exactly one of --deposit or --withdraw-from-target")
request = build_sub_account_spot_transfer(destination, deposit, token, amount, mainnet)
elif kind == "send-asset":
if destination is None:
raise typer.BadParameter("send-asset transfers require a destination address")
request = build_send_asset(amount, destination, token, source_dex, destination_dex, mainnet)
else:
raise typer.BadParameter(f"unknown transfer kind: {kind}")

typer.echo(request.summary)
_confirm("Submit this Hyperliquid transfer via your paired master wallet?", yes)
_submit(request, mainnet)


@money_app.command("deposit", help="Deposit Arbitrum USDC into Hyperliquid Bridge2")
def deposit(
amount: str = typer.Argument(..., help="USDC amount; minimum 5"),
mainnet: bool = typer.Option(False, "--mainnet"),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
) -> None:
_ensure_path()
_refuse_non_interactive_without_yes(yes)
from cli.config import TradingConfig
from cli.hl_actions import build_deposit_transaction
from cli.web_auth import submit_transaction

tx, summary = build_deposit_transaction(amount, mainnet, TradingConfig())
typer.echo(summary)
typer.echo(f"From: {tx['from']}")
typer.echo(f"To: {tx['to']}")
typer.echo(f"Chain ID: {tx['chainId']}")
_confirm("Submit this Arbitrum transaction via your paired master wallet?", yes)
tx_hash = submit_transaction(tx, summary)
typer.echo(json.dumps({"status": "sent", "tx_hash": tx_hash}, indent=2))


@money_app.command("bridge", help="Cross-chain bridge into Arbitrum, then Hyperliquid")
def bridge() -> None:
typer.echo(
"Cross-chain bridge support is intentionally deferred until a provider API, "
"contracts, and calldata are source-verified.",
err=True,
)
raise typer.Exit(2)
3 changes: 2 additions & 1 deletion cli/commands/telegram_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def telegram_start(
)

from tg_bot.config import TelegramBotConfig
from tg_bot.bot import run_bot

config = TelegramBotConfig.from_env()

Expand All @@ -50,6 +49,8 @@ def telegram_start(
)
raise typer.Exit(code=1)

from tg_bot.bot import run_bot

typer.echo(f"Network: {config.default_network}")
typer.echo(f"Chat IDs: {config.allowed_chat_ids or 'auto-detect on first /start'}")
typer.echo("Bot starting... (Ctrl+C to stop)")
Expand Down
6 changes: 2 additions & 4 deletions cli/commands/trade.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,7 @@ def trade_cmd(
os.environ.get("NUNCHI_TRADE_LEDGER_PATH") or str(Path(data_dir) / "trades.jsonl")
)
trade_log.append({
"experiment_id": experiment.experiment_id,
"run_id": experiment.run_id,
"agent_id": experiment.agent_id,
"job_type": experiment.job_type,
**experiment.ledger_fields(),
"ts": int(time.time() * 1000),
"tick": tick_index,
"tick_index": tick_index,
Expand All @@ -169,6 +166,7 @@ def trade_cmd(
"side": fill.side,
"price": str(fill.price),
"quantity": str(fill.quantity),
"notional_usd": str(fill.price * fill.quantity),
"timestamp_ms": fill.timestamp_ms,
"fee": str(fill.fee),
"strategy": "manual_trade",
Expand Down
10 changes: 4 additions & 6 deletions cli/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ def _tick(self) -> None:
"side": fill.side,
"price": str(fill.price),
"quantity": str(fill.quantity),
"notional_usd": str(fill.price * fill.quantity),
"timestamp_ms": fill.timestamp_ms,
"fee": str(fill.fee),
"strategy": self.strategy.strategy_id,
Expand Down Expand Up @@ -437,6 +438,7 @@ def _guard_close_position(self, snapshot: MarketSnapshot) -> None:
"side": fill.side,
"price": str(fill.price),
"quantity": str(fill.quantity),
"notional_usd": str(fill.price * fill.quantity),
"timestamp_ms": fill.timestamp_ms,
"fee": str(fill.fee),
"strategy": self.strategy.strategy_id,
Expand Down Expand Up @@ -523,6 +525,7 @@ def _close_all_positions(self) -> None:
"side": fill.side,
"price": str(fill.price),
"quantity": str(fill.quantity),
"notional_usd": str(fill.price * fill.quantity),
"timestamp_ms": fill.timestamp_ms,
"fee": str(fill.fee),
"strategy": self.strategy.strategy_id,
Expand Down Expand Up @@ -567,12 +570,7 @@ def _log_tick(self, snapshot, decisions, fills, ok: bool) -> None:
def _experiment_fields(self) -> Dict[str, Any]:
if not self.experiment.enabled:
return {}
return {
"experiment_id": self.experiment.experiment_id,
"run_id": self.experiment.run_id,
"agent_id": self.experiment.agent_id,
"job_type": self.experiment.job_type,
}
return self.experiment.ledger_fields()

def _decision_fields(self, decisions=None) -> Dict[str, Any]:
fields: Dict[str, Any] = {"tick_index": self.tick_count}
Expand Down
Loading