Rust Examples
Binance Order With Params
Binance Order With Params — CCXT Rust code example.
// binance-order-with-params — placing an order with exchange-specific extras.
//
// Every unified method ends with a `params` argument for the knobs a
// particular venue understands. On the typed layer that is a `Params` builder,
// so it is written with Rust primitives rather than a dynamic `Value`:
//
// Params::new()
// .with_str("clientOrderId", "my-id-1")
// .with_bool("postOnly", true)
// .with_str("selfTradePrevention", "EXPIRE_MAKER")
//
// The unified ones (`clientOrderId`, `postOnly`, `timeInForce`, `reduceOnly`,
// `triggerPrice`, …) mean the same thing on every venue; ccxt translates them
// into whatever binance actually wants on the wire — `postOnly` becomes a
// LIMIT_MAKER order type on spot, `clientOrderId` becomes `newClientOrderId`.
//
// cargo run --release --bin binance-order-with-params
//
// With no credentials it prints what would be sent and stops.
//
// With credentials it runs against the binance **testnet**
// (`Config::sandbox(true)`); BINANCE_LIVE=1 targets the live host instead.
// The resolved endpoint is printed rather than assumed, so you can always see
// where an order is actually going.
//
// Placing anything still needs an explicit opt-in:
//
// BINANCE_APIKEY=… BINANCE_SECRET=… -> validate-only
// BINANCE_APIKEY=… BINANCE_SECRET=… BINANCE_PLACE_REAL=1 -> places for real
//
// The default path uses binance's `params.test = true` endpoint, which runs
// full validation and creates nothing. The opt-in path is capped at 25 USD
// notional per order and cancels whatever it creates.
use ccxt::types::{Market, Order};
use ccxt::{Binance, Config, ExchangeError, Params};
/// Hard cap on the notional of any order this example is willing to send.
const MAX_NOTIONAL_USD: f64 = 25.0;
const SYMBOL: &str = "BTC/USDT";
fn creds() -> Option<(String, String)> {
match (
std::env::var("BINANCE_APIKEY"),
std::env::var("BINANCE_SECRET"),
) {
(Ok(k), Ok(s)) if !k.is_empty() && !s.is_empty() => Some((k, s)),
_ => None,
}
}
/// A client order id unique per run. Binance rejects a repeat within its
/// retention window with DuplicateOrderId, which is the whole point of the id.
fn client_id(tag: &str, nonce: u128) -> String {
format!("ccxt-{tag}-{nonce}")
}
fn first_line(m: &str) -> String {
m.lines().next().unwrap_or("").chars().take(160).collect()
}
fn show_err(label: &str, e: &ExchangeError) {
println!(" {label:<10} [{}] {}", e.kind, first_line(&e.message));
}
fn show_order(label: &str, o: &Order) {
println!(
" {label:<10} id={} clientOrderId={} status={} type={} {} {:?} @ {:?}",
o.id.clone().unwrap_or_default(),
o.client_order_id.clone().unwrap_or_else(|| "—".into()),
o.status.clone().unwrap_or_default(),
o.order_type.clone().unwrap_or_default(),
o.side.clone().unwrap_or_default(),
o.amount,
o.price,
);
}
/// Refuse to send anything worth `MAX_NOTIONAL_USD` or more.
fn guard(amount: f64, price: f64) -> Result<(), String> {
let notional = amount * price;
if notional >= MAX_NOTIONAL_USD {
return Err(format!(
"{notional:.2} USD >= {MAX_NOTIONAL_USD:.0} cap — refusing"
));
}
Ok(())
}
/// Smallest amount that clears BOTH the min-amount and min-cost limits *after*
/// precision rounding.
///
/// The subtlety: ccxt runs `amountToPrecision` on the way out, which TRUNCATES
/// to the market's amount step. Sizing to exactly the minimum therefore lands
/// just under it once truncated, and binance rejects with
/// `Filter failure: NOTIONAL`. So round UP to the step, then keep adding steps
/// until the notional really clears `limits.cost.min`.
fn min_amount_at(m: &Market, price: f64) -> f64 {
let min_amount = m.limits.amount.min.unwrap_or(0.0001);
let min_cost = m.limits.cost.min.unwrap_or(5.0);
let step = m.precision.amount.filter(|s| *s > 0.0).unwrap_or(0.0);
let mut amount = f64::max(min_amount, min_cost / price);
if step > 0.0 {
// `+ 1e-9` absorbs the float error in the division before ceil().
amount = ((amount / step) + 1e-9).ceil() * step;
// Truncation can still shave a hair off; add steps until it clears.
for _ in 0..8 {
if amount * price >= min_cost && amount >= min_amount {
break;
}
amount += step;
}
}
amount
}
/// Where requests will actually go, read back off the constructed exchange
/// rather than assumed. Worth doing for anything that places orders.
fn api_host(ex: &Binance) -> String {
let api = ccxt::runtime::get_value(&ex.urls, &ccxt::Value::Str("api".to_string()));
match ccxt::runtime::get_value(&api, &ccxt::Value::Str("public".to_string())) {
ccxt::Value::Str(s) => s,
_ => "?".to_string(),
}
}
async fn run() {
let has_creds = creds().is_some();
let place_real = std::env::var("BINANCE_PLACE_REAL").is_ok();
// Nonce for the client ids — passed in rather than generated per call so
// all four stages of one run are grouped by the same suffix.
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
println!("binance-order-with-params");
println!(
" credentials: {} mode: {} cap: {MAX_NOTIONAL_USD:.0} USD/order",
if has_creds { "present" } else { "none" },
if !has_creds {
"public only"
} else if place_real {
"PLACES ORDERS (BINANCE_PLACE_REAL=1)"
} else {
"validate-only (params.test)"
}
);
let mut cfg = Config::new();
if std::env::var("CCXT_VERBOSE").is_ok() {
cfg = cfg.verbose(true);
}
if let Some((k, s)) = creds() {
cfg = cfg.api_key(&k).secret(&s);
// if !live {
// cfg = cfg.sandbox(true);
// }
}
let mut ex = Binance::with_config(cfg);
// Print the resolved host instead of asserting one — sandbox routing does
// not currently work, so an assumption here would be a dangerous lie.
println!(" endpoint: {}", api_host(&ex));
if let Err(e) = ex.try_load_markets(false).await {
println!(
"\nload_markets failed [{}]: {}",
e.kind,
first_line(&e.message)
);
return;
}
ex.set_verbose(true);
let market = match ex.market(SYMBOL) {
Ok(m) => m,
Err(e) => {
println!("\n{}: {}", e.kind, first_line(&e.message));
return;
}
};
let last = match ex.fetch_ticker(SYMBOL, Params::none()).await {
Ok(t) => t.last.unwrap_or(0.0),
Err(e) => {
println!("\nno reference price [{}]", e.kind);
return;
}
};
// Below the book so a limit buy rests instead of filling, but NOT so far
// that binance's PERCENT_PRICE_BY_SIDE filter rejects it — that filter
// bounds how far a limit price may sit from the reference price, and a
// 50%-below bid trips it on BTC/USDT.
let resting_price = (last * 0.90).floor();
let amount = min_amount_at(&market, resting_price);
println!("\n {SYMBOL} last={last:.2} resting bid={resting_price:.2} amount={amount:.6}");
// ── The parameter set ───────────────────────────────────────────────────
let build_params = |tag: &str| {
Params::new()
.with_str("clientOrderId", &client_id(tag, nonce))
.with_bool("postOnly", true)
.with_str("selfTradePrevention", "EXPIRE_MAKER")
};
println!("\n── 2. real order, cancelled afterwards");
let rest_id = client_id("rest", nonce);
if let Err(why) = guard(amount, resting_price) {
println!(" skipped {why}");
} else {
match ex
.create_order(
SYMBOL,
"limit",
"buy",
amount,
Some(resting_price),
build_params("rest"),
)
.await
{
Err(e) => show_err("rejected", &e),
Ok(o) => {
show_order("created", &o);
// ── 3. Read it back by YOUR id, not the venue's ─────────────
println!("\n── 3. read back by clientOrderId");
let by_client = Params::new().with_str("clientOrderId", &rest_id);
match ex.fetch_order("", Some(SYMBOL), by_client).await {
Ok(f) => show_order("found", &f),
Err(e) => show_err("lookup", &e),
}
// ── 4. Cancel by the same id ────────────────────────────────
println!("\n── 4. cancel by clientOrderId");
let cancel_by_client = Params::new().with_str("clientOrderId", &rest_id);
match ex.cancel_order("", Some(SYMBOL), cancel_by_client).await {
Ok(c) => show_order("cancelled", &c),
Err(e) => {
show_err("cancel", &e);
// Fall back to the venue id so nothing is left resting.
if let Some(id) = o.id.clone() {
match ex.cancel_order(&id, Some(SYMBOL), Params::none()).await {
Ok(c) => show_order("cancelled", &c),
Err(e) => {
show_err("cancel!", &e);
if e.is("OrderNotFound") {
println!(
" the order is already gone — filled or cancelled."
);
} else {
println!(" COULD NOT CANCEL — check {SYMBOL} manually.");
}
}
}
}
}
}
}
}
}
// ── 5. postOnly actually refusing to cross ──────────────────────────────
println!("\n── 5. postOnly buy priced above the market (expect a rejection)");
let crossing_price = (last * 1.02).ceil(); // inside the filter band, still crosses
let crossing_amount = min_amount_at(&market, crossing_price);
if let Err(why) = guard(crossing_amount, crossing_price) {
println!(" skipped {why}");
} else {
let p = build_params("cross");
match ex
.create_order(
SYMBOL,
"limit",
"buy",
crossing_amount,
Some(crossing_price),
p,
)
.await
{
Err(e) => {
show_err("rejected", &e);
println!(" postOnly honoured — the order never crossed the spread.");
}
Ok(o) => {
// Should not happen; clean up immediately if it does.
show_order("FILLED?", &o);
println!(" unexpected: postOnly did not prevent a crossing order.");
if let Some(id) = o.id.clone() {
let _ = ex.cancel_order(&id, Some(SYMBOL), Params::none()).await;
}
}
}
}
// Nothing should be left open. Say so either way.
println!("\n── final open orders on {SYMBOL}");
match ex
.fetch_open_orders(Some(SYMBOL), None, None, Params::none())
.await
{
Ok(open) if open.is_empty() => println!(" none — clean."),
Ok(open) => {
println!(" {} still open:", open.len());
for o in &open {
show_order("open", o);
}
}
Err(e) => show_err("lookup", &e),
}
}
fn main() {
if std::env::var("CCXT_SHOW_PANICS").is_err() {
std::panic::set_hook(Box::new(|_| {}));
}
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.thread_stack_size(64 * 1024 * 1024)
.enable_all()
.build()
.unwrap();
rt.block_on(run());
}