Skip to content
Merged
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
47 changes: 40 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ allium realtime balances history \

**Options:** `--chain`, `--address` (repeatable, paired), `--start-timestamp`, `--end-timestamp`, `--limit`, `--body`

#### Holdings

```bash
# Historical Wallet holdings
allium realtime holdings history \
--chain ethereum \
--address 0x3c96937a5bce135c47133702d54b652498e5e375 \
--start-timestamp 2024-03-01T00:00:00Z \
--end-timestamp 2026-03-21T00:00:00Z \
--granularity 1d
```

**Options:** `--chain`, `--address` (repeatable), `--granularity`, `--body`

#### Transactions

```bash
Expand All @@ -140,18 +154,37 @@ allium realtime transactions \

#### PnL

```bash
# Wallet profit and loss
allium realtime pnl \
--chain ethereum --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
##### Latest PnL

# With historical breakdown
allium realtime pnl \
```bash
# Latest Wallet profit and loss
allium realtime pnl latest \
--chain ethereum \
--address 0x3c96937a5bce135c47133702d54b652498e5e375 \
--min-liquidity 1000

# Latest Wallet profit and loss with historical breakdown
allium realtime pnl latest \
--chain ethereum --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \
--with-historical-breakdown
```

**Options:** `--chain`, `--address` (repeatable), `--with-historical-breakdown`, `--body`
**Options:** `--chain`, `--address` (repeatable), `--min-liquidity`, `--with-historical-breakdown`, `--body`

##### Historical PnL

```bash
# Historical Wallet profit and loss
allium realtime pnl history \
--chain ethereum \
--address 0x3c96937a5bce135c47133702d54b652498e5e375 \
--start-timestamp 2024-03-01T00:00:00Z \
--end-timestamp 2026-03-21T00:00:00Z \
--min-liquidity 1000 \
--granularity 1d
```

**Options:** `--chain`, `--address` (repeatable), `--granularity`, `--min-liquidity`, `--body`

---

Expand Down
196 changes: 170 additions & 26 deletions cli/commands/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
from cli.utils.body import load_body_or_build, pair_chain_items
from cli.utils.errors import output_response, resolve_client
from cli.utils.options import chain_address_options, chain_token_options
from cli.utils.time import (
RANGE_END_TIMESTAMP_HELP,
RANGE_START_TIMESTAMP_HELP,
default_range_end_timestamp_utc,
default_range_start_timestamp_utc,
)


@click.group()
Expand All @@ -20,8 +26,9 @@ def realtime(ctx: click.Context) -> None:
prices token prices from on-chain DEX trades
tokens token metadata, search, and lookup
balances wallet token balances (current and historical)
holdings wallet token holdings (historical)
transactions wallet transaction activity with labels
pnl wallet profit and loss calculations
pnl wallet profit and loss calculations (current and historical)
"""


Expand Down Expand Up @@ -109,12 +116,18 @@ def build() -> dict[str, Any]:
@prices.command("history")
@chain_token_options
@click.option(
"--start-timestamp", required=False, help="Start of time range (ISO 8601)."
"--start-timestamp",
default=default_range_start_timestamp_utc,
help=RANGE_START_TIMESTAMP_HELP,
)
@click.option(
"--end-timestamp",
default=default_range_end_timestamp_utc,
help=RANGE_END_TIMESTAMP_HELP,
)
@click.option("--end-timestamp", required=False, help="End of time range (ISO 8601).")
@click.option(
"--time-granularity",
required=False,
default="1d",
type=click.Choice(["15s", "1m", "5m", "1h", "1d"]),
help="Aggregation interval for price data.",
)
Expand All @@ -124,9 +137,9 @@ async def prices_history(
ctx: click.Context,
chain: tuple[str, ...],
token_address: tuple[str, ...],
start_timestamp: str | None,
end_timestamp: str | None,
time_granularity: str | None,
start_timestamp: str,
end_timestamp: str,
time_granularity: str,
body: str | None,
) -> None:
"""fetch historical price series for tokens over a time range.
Expand All @@ -137,13 +150,12 @@ async def prices_history(

def build() -> dict[str, Any]:
addresses = pair_chain_items(chain, token_address, address_key="token_address")
payload: dict[str, Any] = {"addresses": addresses}
if start_timestamp:
payload["start_timestamp"] = start_timestamp
if end_timestamp:
payload["end_timestamp"] = end_timestamp
if time_granularity:
payload["time_granularity"] = time_granularity
payload: dict[str, Any] = {
"addresses": addresses,
"start_timestamp": start_timestamp,
"end_timestamp": end_timestamp,
"time_granularity": time_granularity,
}
return payload

payload = load_body_or_build(body, build)
Expand Down Expand Up @@ -310,9 +322,15 @@ def build() -> list[dict[str, str]]:
@balances.command("history")
@chain_address_options
@click.option(
"--start-timestamp", required=False, help="Start of time range (ISO 8601)."
"--start-timestamp",
default=default_range_start_timestamp_utc,
help=RANGE_START_TIMESTAMP_HELP,
)
@click.option(
"--end-timestamp",
default=default_range_end_timestamp_utc,
help=RANGE_END_TIMESTAMP_HELP,
)
@click.option("--end-timestamp", required=False, help="End of time range (ISO 8601).")
@click.option(
"--limit",
default=None,
Expand All @@ -325,21 +343,19 @@ async def balances_history(
ctx: click.Context,
chain: tuple[str, ...],
address: tuple[str, ...],
start_timestamp: str | None,
end_timestamp: str | None,
start_timestamp: str,
end_timestamp: str,
limit: int | None,
body: str | None,
) -> None:
"""fetch historical token balance snapshots over a time range."""
"""fetch historical token balance snapshots (raw) over a time range."""
client = resolve_client(ctx)

def build() -> dict[str, Any]:
addresses = pair_chain_items(chain, address)
payload: dict[str, Any] = {"addresses": addresses}
if start_timestamp:
payload["start_timestamp"] = start_timestamp
if end_timestamp:
payload["end_timestamp"] = end_timestamp
payload["start_timestamp"] = start_timestamp
payload["end_timestamp"] = end_timestamp
return payload

payload = load_body_or_build(body, build)
Expand Down Expand Up @@ -405,27 +421,95 @@ def build() -> list[dict[str, str]]:
output_response(ctx, resp)


# holdings


@realtime.group()
@click.pass_context
def holdings(ctx: click.Context) -> None:
"""wallet token holdings"""


@holdings.command("history")
@chain_address_options
@click.option(
"--start-timestamp",
default=default_range_start_timestamp_utc,
help=RANGE_START_TIMESTAMP_HELP,
)
@click.option(
"--end-timestamp",
default=default_range_end_timestamp_utc,
help=RANGE_END_TIMESTAMP_HELP,
)
@click.option(
"--granularity",
required=False,
default="1d",
type=click.Choice(["15s", "1m", "5m", "1h", "1d"]),
help="Aggregation interval for holdings data.",
)
@click.pass_context
@async_command
async def holdings_history(
ctx: click.Context,
chain: tuple[str, ...],
address: tuple[str, ...],
start_timestamp: str,
end_timestamp: str,
granularity: str,
body: str | None,
) -> None:
"""calculate historical token holdings (USD value) over a time range."""
client = resolve_client(ctx)

def build() -> dict[str, Any]:
addresses = pair_chain_items(chain, address)
payload: dict[str, Any] = {"addresses": addresses}
payload["start_timestamp"] = start_timestamp
payload["end_timestamp"] = end_timestamp
payload["granularity"] = granularity
return payload

payload = load_body_or_build(body, build)
resp = await client.post("/api/v1/developer/wallet/holdings/history", json=payload)
output_response(ctx, resp)


# pnl


@realtime.command("pnl")
@realtime.group()
@click.pass_context
def pnl(ctx: click.Context) -> None:
"""wallet token PnL, current or historical."""


@pnl.command("latest")
@chain_address_options
@click.option(
"--with-historical-breakdown",
is_flag=True,
default=False,
help="Include historical breakdown over time.",
)
@click.option(
"--min-liquidity",
default=0.0,
type=float,
help="Minimum liquidity for token pairs.",
)
@click.pass_context
@async_command
async def pnl(
async def pnl_latest(
ctx: click.Context,
chain: tuple[str, ...],
address: tuple[str, ...],
with_historical_breakdown: bool,
min_liquidity: float,
body: str | None,
) -> None:
"""calculate realized and unrealized profit and loss for wallets.
"""calculate current realized and unrealized profit and loss for wallets.

optionally include a historical breakdown over time with
--with-historical-breakdown.
Expand All @@ -437,9 +521,69 @@ def build() -> list[dict[str, str]]:

payload = load_body_or_build(body, build)
params: dict[str, Any] = {}
if min_liquidity:
params["min_liquidity"] = min_liquidity
if with_historical_breakdown:
params["with_historical_breakdown"] = "true"
resp = await client.post(
"/api/v1/developer/wallet/pnl", json=payload, params=params
)
output_response(ctx, resp)


@pnl.command("history")
@chain_address_options
@click.option(
"--start-timestamp",
default=default_range_start_timestamp_utc,
help=RANGE_START_TIMESTAMP_HELP,
)
@click.option(
"--end-timestamp",
default=default_range_end_timestamp_utc,
help=RANGE_END_TIMESTAMP_HELP,
)
@click.option(
"--granularity",
required=False,
default="1d",
type=click.Choice(["15s", "1m", "5m", "1h", "1d"]),
help="Aggregation interval for PnL data.",
)
@click.option(
"--min-liquidity",
default=0.0,
type=float,
help="Minimum liquidity for token pairs.",
)
@click.pass_context
@async_command
async def pnl_history(
ctx: click.Context,
chain: tuple[str, ...],
address: tuple[str, ...],
start_timestamp: str,
end_timestamp: str,
min_liquidity: float,
granularity: str,
body: str | None,
) -> None:
"""calculate historical realized and unrealized PnL over a time range."""
client = resolve_client(ctx)

def build() -> dict[str, Any]:
addresses = pair_chain_items(chain, address)
payload: dict[str, Any] = {"addresses": addresses}
payload["start_timestamp"] = start_timestamp
payload["end_timestamp"] = end_timestamp
payload["granularity"] = granularity
return payload

payload = load_body_or_build(body, build)
params: dict[str, Any] = {}
if min_liquidity:
params["min_liquidity"] = min_liquidity
resp = await client.post(
"/api/v1/developer/wallet/pnl/history", json=payload, params=params
)
output_response(ctx, resp)
19 changes: 19 additions & 0 deletions cli/utils/time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from datetime import UTC, datetime, timedelta

RANGE_START_TIMESTAMP_HELP = (
"Range start; e.g. 2026-03-21T00:00:00Z (ISO 8601, Z) or Unix epoch seconds. "
"Default: 30 days ago (UTC)."
)
RANGE_END_TIMESTAMP_HELP = (
"Range end; e.g. 2026-03-21T23:59:59Z (ISO 8601, Z) or Unix epoch seconds. "
"Default: now (UTC)."
)


def default_range_start_timestamp_utc() -> str:
dt = datetime.now(UTC) - timedelta(days=30)
return dt.replace(microsecond=0).strftime("%Y-%m-%dT%H:%M:%SZ")


def default_range_end_timestamp_utc() -> str:
return datetime.now(UTC).replace(microsecond=0).strftime("%Y-%m-%dT%H:%M:%SZ")
Loading
Loading