diff --git a/book/src/metrics/README.md b/book/src/metrics/README.md index cfac286..0647bd2 100644 --- a/book/src/metrics/README.md +++ b/book/src/metrics/README.md @@ -3,12 +3,28 @@ Every metric is derived from Nostr events published by the node itself. No value is self-reported by the operator, and no value is a composite or weighted score. +## Index + +| Metric | Measures | +|---|---| +| [Longevity](longevity-liveness.md#longevity) | How long the node has been operating | +| [Liveness](longevity-liveness.md#liveness) | Whether it is still trading now | +| [Activity consistency](longevity-liveness.md#activity-consistency) | How evenly its trading is spread across the last 30 days | +| [Cumulative performance](trade-size-consistency.md#cumulative-performance) | Lifetime trade count and total volume | +| [Trade size](trade-size-consistency.md#trade-size) | The distribution of individual trade amounts | +| [Dispute signals](disputes-bond-policy.md#dispute-signals) | Dispute counts by outcome, and the rate per 100 trades | +| [Bond policy](disputes-bond-policy.md#bond-policy) | Whether the node requires a trader bond | +| [Fiat currency breakdown](context-signals.md#fiat-currency-breakdown) | Which currencies its trades settle in | +| [Payment method breakdown](context-signals.md#payment-method-breakdown) | Which payment methods its orders accept | +| [Premium signal](context-signals.md#premium-signal) | Its pricing relative to market rate, and how much that varies | +| [Activity grid](activity-grid.md) | When that trading actually happened, over time | + ## Event scoping -Before any computation, each fetched event must satisfy three conditions or it is +Before any computation, a fetched event must satisfy three conditions or it is discarded: its author must be the queried pubkey, its `z` tag must match the value expected for its kind, and its `y` tag must be `mostro`. Events of any other kind are -excluded outright. This guarantees a report never mixes in another node's activity or +excluded outright. A report therefore never mixes in another node's activity, or another application's use of the same kinds. | Kind | `z` value | Backs | @@ -18,58 +34,9 @@ another application's use of the same kinds. | `38385` | `info` | Bond policy | | `38386` | `dispute` | Dispute signals | -## Not-applicable semantics +## Not-applicable values A metric that cannot be computed reports its absence explicitly: `N/A` in -console/plain-text output, `null` in JSON. A `0` is always a computed result, never a -placeholder — a node can genuinely have zero disputes across a long trade history. The -table below states the exact condition per field. - -## Field reference - -| Field | Type | Source | Not applicable when | -|---|---|---|---| -| `stats.longevity.first_seen_at` | string, RFC 3339 UTC | `8383` `created_at` | No dev-fee event exists | -| `stats.longevity.days_active` | number, days | `8383`, falling back to `38383` | No dev-fee event and no qualifying order | -| `stats.cumulative.total_successful_trades` | integer | `38383` `s=success` | Never | -| `stats.cumulative.total_volume_sats` | integer, sats | `38383` tag `amt` | Never | -| `stats.trade_size.min_trade_sats` | integer, sats | `38383` tag `amt` | No qualifying order with a parseable `amt` | -| `stats.trade_size.max_trade_sats` | integer, sats | `38383` tag `amt` | No qualifying order with a parseable `amt` | -| `stats.trade_size.mean_trade_sats` | number, sats | `38383` tag `amt` | No qualifying order with a parseable `amt` | -| `stats.trade_size.median_trade_sats` | number, sats | `38383` tag `amt` | No qualifying order with a parseable `amt` | -| `stats.trade_size.std_dev_trade_sats` | number, sats | `38383` tag `amt` | No qualifying order with a parseable `amt` | -| `stats.trade_size.coefficient_of_variation` | number, ratio | Derived from std dev and median | Fewer than 2 orders, or median is exactly `0` | -| `stats.liveness.last_successful_trade_at` | string, RFC 3339 UTC | `38383` `created_at` | No successful order | -| `stats.liveness.days_since_last_trade` | integer, days | Derived from `last_successful_trade_at` | No successful order | -| `stats.liveness.successful_trades_last_7d` | integer | `38383` `created_at` | Never | -| `stats.liveness.successful_trades_last_30d` | integer | `38383` `created_at` | Never | -| `stats.liveness.successful_trades_last_90d` | integer | `38383` `created_at` | Never | -| `stats.consistency.active_days_last_30d` | integer, days | `38383` `created_at` | Never | -| `stats.consistency.max_consecutive_inactive_days_last_30d` | integer, days | `38383` `created_at` | Never | -| `stats.disputes.total_disputes` | integer | `38386`, deduplicated by `d` | Never | -| `stats.disputes.resolved_disputes` | integer | `38386` tag `s` | Never | -| `stats.disputes.active_disputes` | integer | `38386` tag `s` | Never | -| `stats.disputes.unknown_status_disputes` | integer | `38386` tag `s` | Never | -| `stats.disputes.disputes_per_100_trades` | number, rate | `38386` and `38383` | Zero successful trades | -| `stats.fiat_breakdown.orders_considered` | integer | `38383` tag `f` | Never | -| `stats.fiat_breakdown.distribution` | array of `{currency, orders, share_percent}` | `38383` tag `f` | No order carries a non-empty `f` | -| `stats.payment_method_breakdown.total_mentions` | integer | `38383` tag `pm` | Never | -| `stats.payment_method_breakdown.distribution` | array of `{method, mentions, share_percent}` | `38383` tag `pm` | No `pm` mentions exist | -| `stats.premium.premium_baseline_percent` | number, percent | `38383` tag `premium` | Fewer than 2 orders with a valid `premium` | -| `stats.premium.premium_dispersion_percent` | number, percent | `38383` tag `premium` | Fewer than 2 orders with a valid `premium` | -| `stats.bond_policy.status` | string enum | `38385` tag `bond_enabled` | Never (`unknown` is itself a value) | -| `activity.granularity` | string enum | Derived from the range span | Zero successful orders and no explicit range | -| `activity.range_start` | string, RFC 3339 UTC | Range bounds or order timestamps | Zero successful orders and no explicit range | -| `activity.range_end` | string, RFC 3339 UTC | Range bounds or order timestamps | Zero successful orders and no explicit range | -| `activity.buckets` | array of `{bucket_start, successful_trades, volume_sats, median_trade_sats}` | `38383` | Zero successful orders and no explicit range | - -## Pages - -Each page below defines its metrics, states how they are computed, and describes what -decision they support. - -- [Longevity and liveness](longevity-liveness.md) -- [Trade size and consistency](trade-size-consistency.md) -- [Disputes and bond policy](disputes-bond-policy.md) -- [Fiat, payment method, and premium](context-signals.md) -- [Activity grid](activity-grid.md) +console and plain-text output, `null` in JSON. A `0` is always a computed result, never +a placeholder — a node can genuinely have zero disputes across a long trade history. +Each metric states its own condition. diff --git a/book/src/metrics/activity-grid.md b/book/src/metrics/activity-grid.md index 075b034..7aa2262 100644 --- a/book/src/metrics/activity-grid.md +++ b/book/src/metrics/activity-grid.md @@ -2,38 +2,36 @@ **Definition.** A time-bucketed view of trading activity: one row per interval, each reporting the successful-trade count, sats volume, and median trade size within that -interval. +interval. Where cumulative performance reports lifetime totals, the grid reports how +those totals are distributed over time. **Computation.** Built from the [qualifying successful orders](trade-size-consistency.md#qualifying-orders), assigned to buckets by `created_at`. Buckets are contiguous and gap-free across the full range — an interval with no trades is emitted with zero counts and a null median, not omitted, so -row position corresponds to elapsed time. +row position corresponds to elapsed time. Range and bucket width are resolved by the +rules below. -Where cumulative performance reports totals, the grid reports their distribution over -time. +**Not applicable.** The whole grid — granularity, both range bounds, and the bucket +list — when the node has no qualifying orders and no explicit range was requested. -## Range resolution +**Usability.** Distinguishes sustained activity from a single burst at equal lifetime +totals, and locates when a node's trading started, peaked, or stopped. -**Definition.** The interval the grid spans, reported as `activity.range_start` and -`activity.range_end`. +## Range resolution -**Computation.** With neither `--since` nor `--until`, the range is inferred from the -earliest and latest qualifying order. An explicit bound overrides inference and is -authoritative even when it contains no orders: the grid then emits every bucket across -the requested range with zero counts. +With neither `--since` nor `--until`, the range is inferred from the earliest and latest +qualifying order. An explicit bound overrides inference and is authoritative even when +it contains no orders: the grid then emits every bucket across the requested range with +zero counts. -That case is distinct from the null result reserved for a node with no qualifying -orders and no explicit range. An empty populated grid asserts that no activity occurred -in a specific interval; a null grid asserts that no interval could be determined. The -two are not interchangeable. +That case is distinct from the null result described above. An empty populated grid +asserts that no activity occurred in a specific interval; a null grid asserts that no +interval could be determined. The two are not interchangeable. ## Granularity -**Definition.** The bucket width: `daily`, `monthly`, or `yearly`. - -**Computation.** `--view` sets it explicitly. Otherwise it is selected from the range -span: +`--view` sets the bucket width explicitly. Otherwise it is selected from the range span: | Range span | Granularity | |---|---| @@ -41,32 +39,28 @@ span: | ≤ 730 days | `monthly` | | > 730 days | `yearly` | -Once granularity is known, both range bounds are snapped outward to the enclosing -bucket boundary — a monthly grid reports the first and last instant of the enclosing -calendar months, not the raw order timestamps. When snapping widens the range, the -widened interval is also what gets counted, so the grid never claims to cover a period -it excludes orders from. +Once granularity is known, both range bounds are snapped outward to the enclosing bucket +boundary — a monthly grid reports the first and last instant of the enclosing calendar +months, not the raw order timestamps. When snapping widens the range, the widened +interval is also what gets counted, so the grid never claims to cover a period it +excludes orders from. **Threshold derivation.** The two boundaries are reasoned from output legibility, not measured: a daily grid over a two-year span yields more than 700 rows, which exceeds what a terminal table can usefully present. They are selected to keep row count bounded at each tier, and are not derived from a statistical property of the data. -Because `--view` can force `daily` over a span the automatic rule would never select, -a warning naming the resulting row count is written to stderr whenever that occurs. The +Because `--view` can force `daily` over a span the automatic rule would never select, a +warning naming the resulting row count is written to stderr whenever that occurs. The warning reuses the same 90-day boundary, so it cannot disagree with the selection rule. -**Usability.** Distinguishes sustained activity from a single burst at equal totals, -and locates when a node's activity started, peaked, or stopped. - ## Progress indicator threshold -**Definition.** A status line written to stderr when a relay fetch exceeds 3 seconds, -distinguishing a slow fetch from a stalled process. Suppressed by `--quiet`. +Not a metric — process feedback, documented here because it is the report's other +numeric threshold. A status line is written to stderr when a relay fetch exceeds 3 +seconds, distinguishing a slow fetch from a stalled process. Suppressed by `--quiet`. **Threshold derivation.** Measured, not reasoned: three connect-and-fetch round trips against `wss://relay.mostro.network` completed in 2.06 s, 1.96 s, and 1.69 s. Nominal single-relay operation centers near 2 seconds, so 3 seconds sits above normal variance while still surfacing a genuinely degraded fetch. - -This threshold governs process feedback only and does not affect any reported metric. diff --git a/book/src/metrics/context-signals.md b/book/src/metrics/context-signals.md index 7d5cc65..8a56305 100644 --- a/book/src/metrics/context-signals.md +++ b/book/src/metrics/context-signals.md @@ -27,6 +27,9 @@ empty-string bucket. Results are ranked by descending share, with ties broken by currency name ascending, so the ordering is deterministic despite the underlying tally being unordered. +**Not applicable.** The distribution, when no qualifying order carries a non-empty `f` +value. The considered-order count is always computed. + **Usability.** Establishes which currencies the node actually settles in, and in what proportion, before committing to a trade denominated in one of them. @@ -42,6 +45,9 @@ orders, and a node whose orders each list three methods reports three times as m mentions as orders. Empty values are filtered defensively, as relay data is untrusted. Ranking follows the same rule as the fiat breakdown. +**Not applicable.** The distribution, when no `pm` mentions exist across the qualifying +orders. The mention count is always computed. + **Usability.** Establishes whether the node supports an intended payment method, and how central that method is to its trading. @@ -60,6 +66,9 @@ undefined for a single point, and a single-point median would misrepresent a bas The standard deviation is population, consistent with [trade size](trade-size-consistency.md#trade-size). +**Not applicable.** Both figures, when fewer than two qualifying orders carry a valid +`premium` value. + **Usability.** The baseline is the premium to expect on a typical order. The dispersion qualifies that expectation: high dispersion means quoted premiums vary substantially between orders, so the baseline is a weak predictor of any individual quote. diff --git a/book/src/metrics/disputes-bond-policy.md b/book/src/metrics/disputes-bond-policy.md index 043221c..ee54077 100644 --- a/book/src/metrics/disputes-bond-policy.md +++ b/book/src/metrics/disputes-bond-policy.md @@ -24,6 +24,9 @@ The rate normalizes the total against `stats.cumulative.total_successful_trades` not applicable only when that denominator is zero. A node with successful trades and no disputes reports `0.0`, which is a computed result, not an absence. +**Not applicable.** The rate alone, when the node has zero successful trades. The four +counts are always computed. + **Usability.** The rate is the comparable figure across nodes; the raw total is not, since it scales with volume. A non-zero `unknown_status_disputes` warrants separate attention: it indicates dispute activity the node's own published data does not fully @@ -44,6 +47,9 @@ maps to `unknown`. states — one confirms no bond is required, the other confirms nothing — and merging them would present an absence of data as a positive finding. +**Not applicable.** Never. Absence of data is reported as `unknown`, which is itself +one of the three values. + **Usability.** Descriptive only. The report does not rank the three statuses, because a bond requirement raises the cost of trading and the cost of bad-faith behavior simultaneously, and which tradeoff is preferable depends on context outside this tool's diff --git a/book/src/metrics/longevity-liveness.md b/book/src/metrics/longevity-liveness.md index 9788860..a032857 100644 --- a/book/src/metrics/longevity-liveness.md +++ b/book/src/metrics/longevity-liveness.md @@ -15,6 +15,9 @@ would freeze the value at the interval between two trades and report `0` for a n with exactly one. The fallback path leaves `first_seen_at` unset, since no dev-fee event exists to date it. +**Not applicable.** Both fields, when the node has neither a dev-fee event nor a +qualifying successful order. `first_seen_at` alone, whenever the fallback path applies. + **Usability.** Establishes how much operating history exists to evaluate. It is a precondition for interpreting the other metrics, not a quality signal on its own: a high `days_active` with zero successful trades describes a node that has existed @@ -30,6 +33,9 @@ trade, elapsed days since it, and successful-trade counts over rolling 7, 30, an orders. The three rolling counts are independent windows, not cumulative buckets — a trade three days old is counted in all three. +**Not applicable.** The last-trade timestamp and elapsed days, when no successful order +exists. The three rolling counts are always computed; zero is a result, not an absence. + **Usability.** Distinguishes an actively used node from a historically active one. Read alongside longevity: strong `days_active` with zero trades in the 90-day window indicates a node that has stopped trading, whatever the reason. @@ -47,6 +53,9 @@ indices whenever report-generation time is not exactly midnight UTC. The inactiv calculation includes the gap preceding the first active day and the gap following the last, not only the gaps between active days. +**Not applicable.** Never. A node with no trades in the window reports zero active days +and a 30-day inactive run, both computed results. + **Usability.** Separates steady trading from bursty trading at equal trade counts. Two nodes with the same 30-day volume differ materially if one traded on 20 days and the other on 2. diff --git a/book/src/metrics/trade-size-consistency.md b/book/src/metrics/trade-size-consistency.md index 926ee5a..9ea8189 100644 --- a/book/src/metrics/trade-size-consistency.md +++ b/book/src/metrics/trade-size-consistency.md @@ -21,6 +21,9 @@ of their sats amounts. accumulated with saturating addition, so a crafted extreme `amt` on an untrusted relay event cannot overflow or wrap the total. +**Not applicable.** Never. A node with no qualifying orders reports zero for both, +which is a computed result. + **Usability.** Establishes scale. It is a lower bound on realized activity and carries no recency information — pair it with liveness to determine whether the volume is current or historical. @@ -45,6 +48,11 @@ Over an even-sized set the median is the mean of the two central values and is r at full precision, including fractional sats. Truncating it to an integer would both misstate the median and distort the coefficient of variation computed from it. +**Not applicable.** All six figures, when no qualifying order carries a parseable +`amt`. The coefficient of variation additionally, when fewer than two such orders exist +or the median is exactly `0`, since a zero denominator leaves the ratio undefined +regardless of sample size. + **Usability.** The coefficient of variation is the operative figure: it answers whether a node's trades cluster around a typical size or span a wide range. A low value indicates predictable sizing. A high value is not itself adverse — it may reflect a node