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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Run `make verify` before considering a change done — it's exactly what CI runs
- Open orders come only from `orders::open_orders` (the `all=true` list,
filtered client-side): the `?propertyId=` query reports a partially filled
order's original quantity.
- `portfolio.rs` holds the pure report logic behind `account portfolio`
(holdings weighted by location, property, and rent); `account.rs` only
fetches its inputs.
- `src/client.rs` — HTTP against the SDK surface (`/public/v1`) and the internal
website API (`--internal`, `/prod`).
- `src/config.rs` — non-secret config; the API key is keychain-only.
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ $ lofty rewards history --since <ms> # your reward payouts
$ lofty rewards reconcile # audit payouts: totals, math check, gap detection
$ lofty rewards eligibility # are your orders earning right now, and if not why
$ lofty account balance|positions|trades
$ lofty account portfolio # holdings weighted by state, city, property, and rent
$ lofty account coverage # what's reserved backing your bids vs free to spend
$ lofty account breakeven --margin 5 # fee-inclusive sell price (basis hides the buy fee)
$ lofty account rebates # maker rebates earned, derived from your fills
Expand Down Expand Up @@ -171,6 +172,43 @@ $49.81 | +$0.35 | $52.75 | 01SAMPLEPROP00000000000001 | 4 | sell | $2.94
`orders-competitiveness/v1` with a `book` block and a `buy`/`sell` block per
property (`null` for a side you have no orders on).

### Example — where is my money, and what pays?

`portfolio` weights your holdings by **estimated value** (each position's
`currentValue`, tokens × current price) across states, cities, and properties,
and by **rent**: each property's share of your projected daily rent.

Daily rent is the listing's `projected_annual_cash_flow` per issued token, times
the tokens you hold, over 365 days. A property counts as **renting** unless its
listing marks it vacant (`is_occupied: false`) or delinquent (`is_delinquent`),
or projects no cash flow. Non-paying properties are excluded from the rent
weighting (their share is `-` / `null`, not `0%`), so the renting ones sum to
100%. The headline also gives the share of properties renting and the share of
holdings value that is rent-earning. Numbers below are illustrative.

```console
$ lofty account portfolio
4 properties, $10000.00 estimated value, $1.4000/day projected rent
renting: 2 of 4 properties (50.0%), 70.0% of holdings value

BY STATE
PROPERTIES | RENT % | STATE | VALUE | VALUE %
2 | 57.1% | OH | $6000.00 | 60.0%
1 | 42.9% | MI | $3000.00 | 30.0%
1 | 0.0% | TN | $1000.00 | 10.0%
...
BY PROPERTY
LOCATION | PROPERTY | RENT % | RENT/DAY | STATUS | VALUE | VALUE %
Cleveland, OH | 100 Sample St | 57.1% | $0.8000 | renting | $4000.00 | 40.0%
Toledo, OH | 200 Sample St | - | $0.0000 | vacant | $2000.00 | 20.0%
```

`--json` emits `account-portfolio/v1` with `totals`, `byState`, `byCity`, and
`properties` (each carrying `valueSharePct`, `dailyRentUsd`, `rentSharePct`,
and `rentStatus`: `renting` | `vacant` | `delinquent` | `no-cash-flow` |
`unknown`). A held property whose listing is gone is still weighted by value,
under an `unknown` location, rather than dropped.

### Example — what can I actually spend?

Lofty funds open orders from your live balances: buys need the USDC and sells
Expand Down
41 changes: 41 additions & 0 deletions src/commands/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ pub enum Cmd {
Balance,
/// Token holdings by property, with cost basis and P&L inputs.
Positions,
/// Portfolio composition: holdings weighted by state, city, and property
/// (on estimated value), plus each property's share of your daily rent and
/// how much of the portfolio is renting right now.
///
/// Value is each position's `currentValue` (tokens x current price). Daily
/// rent is the property's projected annual cash flow, per issued token, times
/// the tokens you hold, over 365 days. A property counts as renting unless its
/// listing marks it vacant or delinquent or projects no cash flow; those pay
/// nothing, so they are excluded from the rent weighting rather than shown as
/// a 0% share.
Portfolio,
/// Executed trades (completed buys and sells).
Trades {
/// Filter to one property.
Expand Down Expand Up @@ -129,6 +140,36 @@ pub fn run(ctx: &Ctx, cmd: &Cmd) -> Result<(), CliError> {
});
Ok(())
}
Cmd::Portfolio => {
let positions = client
.get("/public/v1/account/positions", &[])?
.get("positions")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let mut listings = std::collections::BTreeMap::new();
for (pid, _) in super::portfolio::held(&positions) {
match client.get(&format!("/public/v1/properties/{pid}"), &[]) {
// `properties/{id}` wraps its payload in `property`; accept a
// bare listing too, as `venue_fees` does.
Ok(v) => {
let listing = v.get("property").cloned().unwrap_or(v);
listings.insert(pid.to_string(), listing);
}
// A delisted property still counts toward value; the report
// files it under `unknown` instead of failing the whole run.
Err(CliError::NotFound(_)) => {
if !ctx.common.quiet {
eprintln!("{pid}: no listing found — weighted under `unknown`");
}
}
Err(e) => return Err(e),
}
}
let report = super::portfolio::portfolio(&positions, &listings);
emit(ctx, "account-portfolio", report, super::portfolio::render);
Ok(())
}
Cmd::Trades {
property_id,
direction,
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod book;
pub mod catalog;
pub mod competitiveness;
pub mod orders;
pub mod portfolio;
pub mod properties;
pub mod quote;
pub mod rewards;
Expand Down
Loading
Loading