MoonClient maintains the market universe and live market read model for the
application. UI code searches by symbol once, keeps a stable MarketHandle, and
reads prices, funding, tags, balances/positions, arbitrage slots, and retained
history from snapshots/handles.
The runtime owns the server refreshes that feed this state: full market list,
incremental price/funding updates, token tags, correlation prices, and
server-index refresh after reconnect/server restart. Applications should read
the maintained state and events from MoonClient snapshots/events, not parse
market payloads or server indexes themselves.
MarketsState::find(input) / search(input, limit) are the normal terminal
search-box helpers: users may type a full market name (BTCUSDT) or a coin
symbol (BTC, SOL). The result is a stable MarketHandle, not a temporary
borrow. MarketsState::get(name) remains the exact-name path for code that
already has the canonical name.
This follows the production core's stable-market-object model: listing refresh may replace the surrounding list/dictionaries, but existing market objects stay alive and are mutated in place. UI code may keep the handle after a search and read it later without re-searching by name.
use moonproto::TokenTags;
let Some(state) = client.snapshot() else { return; };
let markets = state.markets();
if let Some(market) = markets.find("BTC") {
let pos = market.balance_position();
let price = market.price();
let tail = market.trade_state();
let deltas = market.delta_state();
let session_profit = state.session_profit_for(&market);
let max_pos = market.max_pos_limit();
let protection = state.position_protection_for(&market);
market.with(|market| {
println!(
"tick={} max_lev={} max_order={}",
market.tick_size(),
market.max_leverage,
market.max_order_value()
);
});
println!(
"liq={} bid={} ask={} mark={} last_trade={} coin1h={} max_pos={} session_usd={:?} protected={}",
pos.liq_price,
price.bid,
price.ask,
price.mark_price,
tail.last_trade_price,
deltas.coin_1h_delta,
max_pos,
session_profit.map(|profit| profit.usd),
!protection.both.has_warning
);
}
let global_deltas = markets.global_deltas();
println!("btc1h={} exchange1h={}", global_deltas.btc_1h_delta, global_deltas.exchange_1h_delta);
let tags = markets.tags("BTCUSDT");
if tags.contains(TokenTags::ALPHA) {
println!("BTCUSDT has ALPHA tag");
}Balance and position packets update these same live Market objects. For chart
UI this is the normal path: keep the selected MarketHandle and read fields
such as pos_size, pos_price, liq_price, leverage_x, asset_balance,
total_profit_*, and max_value from balance_position(). BalancesState is the account
totals view, not the primary per-market UI object.
For chart overlays that only need position fields, MarketHandle::balance_position
returns a small copy without cloning the whole market object.
MarketHandle::session_profit() reads the independent per-market session
counter in core base currency. snapshot.session_profit_for(&market) converts
the same retained value to USD for the chart header. Full snapshots and incremental
changes are applied automatically, including recovery after a missed update.
Use client.settings().reset_sessions(Some(market.name()), false) to reset
this market on the core; see session profits.
For the "unprotected position" warning, use
snapshot.position_protection_for(&market): the library counts active
non-emulator SellSet close orders by side, and the UI only decides how to
draw/blink that warning.
For price/funding/mark-price and live trade-tail overlays, use
MarketHandle::price() and MarketHandle::trade_state() on the same retained
handle instead of resolving the market name again.
For signed MoonBot signal deltas, use MarketHandle::delta_state() for the
selected market and MarketsState::global_deltas() for BTC/exchange signals.
These are separate from retained-history range/max-move analytics. If the UI
wants "Exclude blacklisted markets from the market delta calculation"
checkbox, call
client.settings().set_exclude_blacklisted_markets_from_exchange_delta(true);
the runtime then applies coins_black_list_text to retained markets before
computing Exchange1hDelta / Exchange24hDelta.
Markets-table style values are also read from the retained market handle:
market.with(|m| m.max_order_value())is theMax.Ordercolumn: exchangemax_qtyconverted through the current ask price.market.max_pos_limit()is the per-marketMaxPosvalue derived from the latest leverage-management config.0means there is no explicit/wildcard per-market rule; the globaldeffallback remains available throughsnapshot.settings().lev_manage.as_ref().map(|l| l.default_max_pos_limit()).
Arbitrage relay packets also apply to the live market. Use
MarketHandle::arb_slot(ArbPlatformCode::...) or
arb_now(ArbPlatformCode::...) from the
selected handle; raw arb market_index blocks are diagnostic protocol details.
Arb price entries expose time() / unix_millis() helpers; the fixed ring
cursor is diagnostics/test-only.
Use MarketsState::markets_version() to invalidate cached name/symbol lookups,
including "not found" results. Read the revision and perform the lookup on the
same snapshot, so a new list cannot be paired with an older revision:
let Some(state) = client.snapshot() else { return; };
let markets = state.markets();
let revision = markets.markets_version();
let market = markets.find("BTC");
// Cache `market` (including None) with this revision and the query.The revision changes when new market names become visible; a full-list apply may also advance it without adding names. Compare it for equality only within one client, and clear the cache when replacing that client. Include any other lookup inputs, such as quote currency, in your cache key.
This is not a revision of all market data: prices, volumes, deltas, tags, trading status, and server-index order can change independently. Keep reading live values from retained handles; this revision does not invalidate caches that filter or sort by those values.
Call client.markets().refresh_stats()? when the user requests an update. One
request fetches the core's already calculated values for every market; no trade,
orderbook, or candle subscription is needed.
client.markets().refresh_stats()?;
// After Event::Markets(MarketsEvent::StatsUpdated { .. }):
if let Some(stats) = market.stats() {
println!("24h volume={} 1h volume={} 1m delta={}%",
stats.volume_24h, stats.volume_1h, stats.delta_1m);
}MarketStats contains only these values:
| Fields | Meaning |
|---|---|
volume_24h, volume_1h |
24-hour and 1-hour quote-currency volume (f64). |
volume_1m, volume_3m, volume_5m |
Short-window quote-currency volume (f32). |
delta_1m, delta_15m, delta_1h, delta_3h, delta_24h |
Core markets-table percentage deltas (f64). |
delta_72h |
Core markets-table 72-hour percentage delta, rounded by the core (i32). |
These are the latest values calculated by the core, using its delta settings;
the library does not recalculate them from local history. They are separate
from delta_state() trend signals and retained-history analytics. Values are
sampled while the core builds the response, not at one globally atomic instant.
MarketHandle::stats() copies the small value under one read lock. It returns
None before the first successful manual refresh and after a core process restart;
a received all-zero row is Some(MarketStats { .. }). A transport reconnect to
the same core retains the previous values. Successful refreshes also update
Market::volume with the received 24-hour volume.
There is no timer, startup request, automatic retry, or refresh on reconnect.
The return from refresh_stats() means the request was queued. Completion emits
MarketsEvent::StatsUpdated { count }; server rejection, invalid data, or stale
market indexes emit StatsRefreshFailed { message } and preserve prior values.
If initialization or a market-list refresh was in progress, retry manually after
it completes. Connection loss is reported through normal lifecycle events.
A core implementing this operation is required; older cores report an unknown
method failure. Existing automatic price/funding updates remain independent.
Initial fetch:
use moonproto::{ConnectConfig, InitConfig, MoonClient};
let init = InitConfig {
..Default::default()
};
let client = MoonClient::connect(cfg, ConnectConfig::new(init))?;Long-running price refresh is controlled by ClientConfig.refresh. The default
uses the MoonBot core worker cadence, but ticks are gated by Init: transport Fine
does not start background Engine API. Set update_markets_every /
check_tags_every to None if the application owns those requests manually.
See examples/market_refresh.rs for a compact consumer-side loop that reads
prices and tags from MoonClient.
pub enum MarketsEvent {
// Historical name: emitted when a GetMarketsList response was applied.
MarketsListReplaced { count: usize, corr_count: usize },
NewMarketsAdded { names: Vec<String> },
PricesUpdated { count: usize, included_funding: bool, included_corr: bool },
StatsUpdated { count: usize },
StatsRefreshFailed { message: String },
IndexesUpdated { count: usize },
TokenTagsUpdated { count: usize },
}MarketsState is a read API over the live market catalog. Its internal COW
maps/lists and server-index helpers are not the terminal surface. Normal UI code
uses iter(), get() -> MarketHandle, market_snapshot(name), price(name),
tags(name), trade_state(name), delta_state(name), global_deltas(),
exclude_blacklisted_markets_from_exchange_delta(), and the count helpers.
Selected-market UI should keep the MarketHandle returned by get() and read
through that handle.
pub struct MarketPrice {
pub bid: f64,
pub ask: f64,
pub last_bid: f64,
pub last_ask: f64,
pub p_last: f64,
pub min_lot_size: f64,
pub chart_price_step: f64,
pub funding_rate: f64,
pub mark_price: f64,
pub mark_price_found: bool,
}
impl MarketPrice {
pub fn funding_time(self) -> MoonTime;
}pub struct BaseCurrencyPrice {
pub base_currency: String,
pub last_price: f64,
pub usdt_market: Option<String>,
pub usdt_rev_market: Option<String>,
pub usdt_corr_market: Option<String>,
pub usdt_rev_corr_market: Option<String>,
}pub struct MarketTradeState {
pub last_got_all_trades_ms: i64,
pub last_got_spot_trades_ms: i64,
pub last_trade_price: f64,
pub last_buy_price: f64,
pub last_sell_price: f64,
pub last_trade_price_ema15: f64,
pub last_trade_price_ema5: f64,
pub last_trade_was_sell: bool,
}pub struct MarketDeltaState {
pub last_price_ema: f64,
pub coin_1h_avg: f64,
pub coin_24h_avg: f64,
pub coin_1h_delta: f64,
pub coin_1h_delta_ema: f64,
pub coin_24h_delta: f64,
pub coin_24h_delta_ema: f64,
}
pub struct MarketGlobalDeltas {
pub btc_1h_avg: f64,
pub btc_24h_avg: f64,
pub btc_72h_avg: f64,
pub btc_1h_delta: f64,
pub btc_24h_delta: f64,
pub btc_72h_delta: f64,
pub exchange_1h_delta: f64,
pub exchange_24h_delta: f64,
pub exchange_market_count: usize,
}The retained LastPrice line row is:
let price = point.price();
let unix_ms = point.unix_millis();
let time = point.time();UI code should use price(), time(), or unix_millis() instead of carrying
raw protocol time.
This row is the retained LastPrice chart line, not the last trade price. It is
filled from UpdateMarketsList: the server sends Bid/Ask, the client
computes pLast = (Bid + Ask) / 2, and the chart line is drawn from that
retained price history.
The retained-history worker appends a LastPricePoint only when the production
core would add a price-history row: pLast > 0, bid or ask is present, and the
market is a BTC market or a base-USDT market.
The retained MarkPrice line row has the same shape:
let mark_price = point.price();
let mark_time = point.time();It is filled from UpdateMarketsList -> MarketPrice.mark_price when the server
marks the value as present. UI code can compare the MarkPrice line with the
LastPrice line for the same market; both are retained in the same per-market
history model.
When trades retained storage is active, MoonClient appends these rows
immediately after applying market prices. Retained history is created lazily
from the active trades subscription scope, so markets outside
subscribe_trades_for do not allocate price-line rings.
Market::futures_type uses BaseCurrency, a small public wrapper that
preserves unknown future server values:
pub struct BaseCurrency;
BaseCurrency::BTC;
BaseCurrency::USDT;
BaseCurrency::USDC;
BaseCurrency::EMPTY;
BaseCurrency::UNKNOWN;
let label = market.futures_type.name();Known constants cover the currently named server values. Unknown future values
are preserved as their original byte instead of being collapsed to
BaseCurrency::UNKNOWN. For older servers that do not provide this field,
Market::futures_type is BaseCurrency::EMPTY.
Use BaseCurrency::name() for UI labels.
Market::listed_type() returns the core post-processing result for
GetMarketsList: BaseCurrency::EMPTY means
ListedType::SPOT; any other futures_type means ListedType::BOTH.
ListedType is a public ordinal wrapper for the derived listing kind.
Convenience methods:
let Some(state) = client.snapshot() else { return; };
let markets = state.markets();
for handle in markets.iter() {
handle.with(|market| {
println!("{} {}", market.symbol(), market.status_trading);
});
}
let btc = markets.get("BTCUSDT"); // Option<MarketHandle>
let btc_snapshot = markets.market_snapshot("BTCUSDT");
markets.price("BTCUSDT");
markets.ref_btc_corr_market("DOGEUSDT");
markets.base_currency_price("BTC");
markets.trade_state("BTCUSDT");
markets.delta_state("BTCUSDT");
markets.global_deltas();
markets.tags("BTCUSDT");
markets.market_count();
markets.corr_count();Server-index mapping is runtime/diagnostic protocol state. Normal UI code keeps
a MarketHandle or reads by market name. In the normal MoonClient path,
trades and orderbook events are gated until fresh indexes are rebuilt by
cold-init GetMarketsList or refreshed through GetMarketsIndexes after
reconnect/server restart.
pub struct TokenTags;
TokenTags::MONITORING;
TokenTags::FAN;
TokenTags::SEED;
TokenTags::LAUNCH;
TokenTags::GAMING;
TokenTags::NEW;
TokenTags::OLD;
TokenTags::BNB;
TokenTags::ALPHA;
TokenTags::OI_CAPPED;
TokenTags::TRAD_FI;Use contains, is_empty, bits, and from_bits for bitset work.
MoonClient refreshes market metadata, prices, funding, tags, and server-index mappings. It also handles listing refresh and reconnect recovery. Application code does not poll these endpoints or use server market indexes.
- Existing
MarketHandlevalues stay valid while refreshed fields change. - Indexed streams are not applied against a stale server mapping.
MarketsEvent::NewMarketsAddedis emitted only after new markets are present in retained state.- Funding and other timestamps should be read through their
MoonTimehelpers. - Raw merge rules, correlation-index rebuilding, and listing throttles are runtime implementation details rather than terminal control flow.