Sequence/docs

Execution Graphs

Every order is a graph. A market buy is a 1-node graph. A bracket is a 3-node graph (entry → take-profit + stop-loss). A streaming hedge is a 2-node graph with a triggered edge. The graph engine is the universal primitive — same path for everything.

This guide covers the builder shape across both SDKs, every edge Trigger, every Sizing transform, the 4-layer RiskConfig, and a cookbook of common multi-leg recipes (bracket, TWAP, streaming hedge, conditional entry).

For the conceptual deep-dive on why graphs are the universal primitive, see Concepts → Execution Graphs.


Building a graph

rust
use sequence_sdk::{Policy, Urgency};
use sequence_sdk::builders::graph::{Node, Trigger, Sizing, FailureAction};
 
seq.graph()
    .node("spot",  Node::buy ("ETH-USD",      200.0).policy(Policy::Sor))
    .node("hedge", Node::sell("ETH-USD-PERP", 200.0).perp().venue("hyperliquid"))
    .edge("spot", "hedge", Trigger::FillPct(0.5))
    .risk(|r| r
        .max_notional_usd(500_000.0)
        .max_unhedged_qty(5.0)
        .on_runtime_breach(FailureAction::CancelAll))
    .submit().await?;

GraphBuilder methods: .node(id, Node), .edge(from, to, Trigger), .edge_with(from, to, Trigger, |EdgeConfig|), .risk(|RiskConfig|), .sandbox(), .metadata(json), .submit().

Nodes with no incoming edge auto-activate as root; the rest as wait.


Triggers — when an edge fires

TriggerWire formFires when
Trigger::OnAccepted"on_accepted"SOR ACKs the parent (node_order_id assigned)
Trigger::FirstFill"on_first_fill"First partial fill lands
Trigger::OnFill"on_fill"Every fill — fires repeatedly (streaming hedge)
Trigger::FullFill"on_full_fill"Parent 100% filled
Trigger::FillPct(p)"on_fill_ratio" + condition.fill_ratio_gteFill ratio crosses threshold (one-shot)
Trigger::Timeout(ms){"on_timeout": {"ms": ...}}N ms after source node activation
Trigger::OnCancel"on_cancel"Parent cancelled
Trigger::OnDone"on_done"Any terminal state (filled / cancelled / rejected / expired)
Trigger::OnPrice {…}{"on_price": {...}}Price condition on a symbol
Note

OnFill vs FillPct — these are different triggers. OnFill fires on every fill event regardless of ratio (use for streaming hedge). FillPct(0.5) fires once when filled-qty crosses 50%. They write different wire forms (on_fill vs on_fill_ratio); CC ignores condition.fill_ratio_gte when the trigger is on_fill.


Sizing — how child qty is derived

Edges can transform the child node's quantity based on the parent's fill state. The child node must declare .derived() (Rust) or omit a fixed qty (in graph form, set qty to 0 with "derived": true in execution).

SizingEffect
Sizing::ParentFilledQtyChild uses parent's cumulative filled qty
Sizing::IncrementalFillOnly the delta since last edge fire (streaming hedge)
Sizing::LinearHedge(m)Multiply parent filled qty by m (negative = opposite side, e.g. -1.0 = 1:1 short hedge)
Sizing::ScaledNotional { multiplier, cap_usd }Scale by notional, optional USD cap
Sizing::ResidualParent target − parent filled (remainder after partial)
Sizing::Fixed(q)Ignore parent — fixed child qty

Risk — 4-layer model

RiskConfig evaluates at four points in the graph lifecycle. Layer 1+2 are pre-trade; Layer 3 enforces continuously while the graph runs; Layer 4 specifies what happens on breach.

rust
seq.graph()
    .node("spot", Node::buy("BTC-USD", 1.0))
    .risk(|r| r
        // Layer 1 — Admission (before any order leaves)
        .max_notional_usd(500_000.0)
        .max_leverage(3.0)
        .max_concurrent_graphs(5)
 
        // Layer 2 — Pre-trade (per-node gates)
        .max_node_notional_usd(100_000.0)
        .require_balance_check()
        .max_slippage_bps(15)
 
        // Layer 3 — Runtime (continuously)
        .max_unhedged_qty(0.5)
        .max_loss_usd(2_000.0)
        .max_drawdown_usd(5_000.0)
 
        // Layer 4 — Failure action
        .on_runtime_breach(FailureAction::CancelAll)
        .on_disconnect(FailureAction::Pause))
    .submit().await?;

FailureAction variants: CancelAll, CancelPending, Pause, Continue (serialized as cancel_all_open / pause_graph / do_nothing).


Recipes

TWAP — slice over time

Sub-graph expansion happens automatically when a node has .twap(slices, interval_ms) — under the hood the engine spawns N child slice nodes.

rust
seq.graph()
    .node("twap", Node::buy("BTC-USD", 1.0).twap(10, 30_000))
    .submit().await?;

Bracket — entry + take-profit + stop-loss

rust
seq.graph()
    .node("entry", Node::buy ("BTC-USD", 0.1))
    .node("tp",    Node::sell("BTC-USD", 0.0).derived().limit_price(80_000.0))
    .node("sl",    Node::sell("BTC-USD", 0.0).derived())
    .edge("entry", "tp", Trigger::FullFill)
    .edge("entry", "sl", Trigger::OnPrice {
        symbol: "BTC-USD".into(),
        direction: "below".into(),
        offset_pct: -5.0,
    })
    .risk(|r| r.max_loss_usd(2_000.0).on_runtime_breach(FailureAction::CancelAll))
    .submit().await?;

Streaming hedge — every fill triggers proportional perp sell

rust
seq.graph()
    .node("spot",  Node::buy ("ETH-USD",  100.0))
    .node("hedge", Node::sell("ETH-PERP",   0.0).perp().derived())
    .edge_with("spot", "hedge", Trigger::OnFill,
               |e| e.sizing(Sizing::LinearHedge(-1.0)))
    .submit().await?;

OnFill (not FillPct) is the streaming-hedge trigger — it fires on every fill event. LinearHedge(-1.0) sizes the child to the opposite-side fill delta.


Inspect a running graph

rust
let s = seq.graph_status("graph_…").await?;
for (id, n) in &s.nodes {
    println!("{:8} {} {}/{}", id, n.status,
        n.filled_qty_1e8, n.target_qty_1e8);
}

GraphStatusResponse { graph_id, client_id, status, nodes: HashMap<_, NodeStatus>, edges: HashMap<_, EdgeStatus> }. Top-level statuses: pending | active | partial_fill | completed | aborted | expired | paused.


Cancel / resume / amend a node

rust
seq.graph_cancel("graph_…").await?;
seq.graph_resume("graph_…").await?;
 
// Retune execution params on a *running* node (different from amend()
// which works on resting orders)
seq.amend_order("graph_…", "spot", serde_json::json!({
    "urgency": "high",
    "max_slippage_bps": 20,
    "horizon_ms": 60_000,
})).await?;

amend_order (PUT-on-node) updates execution parameters on a running node — different from the flat amend() verb (price/qty on a resting order). Use the flat one for resting amends, this one to retune live execution.


Next steps