Skip to main content

strategy_sdk/
lib.rs

1//! The Rust SDK for agent authors.
2//!
3//! An agent is one long-lived async program. The host starts its `run` once
4//! and never schedules it again: whatever the agent does, it does inside that
5//! call, awaiting the streams and the drivers it cares about.
6//!
7//! Everything it can reach is an import of the `shonin:agent` world, and the
8//! only way out to a venue is [`drivers::submit`]. An agent holds no key and
9//! signs nothing; the executor does that, and the venue and the vault's
10//! contracts between them make sure those signatures can only ever move the
11//! vault's own money between the vault's own accounts.
12//!
13//! An agent's settings are constants in its own code, so `run` takes no
14//! arguments.
15
16#![allow(clippy::too_many_arguments)]
17
18/// The generated bindings for the `shonin:agent` world.
19pub mod bindings {
20    wit_bindgen::generate!({
21        path: "wit",
22        world: "agent",
23        // Only the functions the world itself declares `async` may be lowered
24        // that way; naming them one by one keeps the two in step.
25        async: [
26            "shonin:agent/drivers@0.1.0#submit",
27            "shonin:agent/drivers@0.1.0#account-state",
28            "shonin:agent/transfers@0.1.0#move",
29            "shonin:agent/transfers@0.1.0#bridge",
30            "shonin:agent/transfers@0.1.0#balances",
31            "shonin:agent/data@0.1.0#query",
32            "shonin:agent/websocket@0.1.0#connect",
33            "shonin:agent/websocket@0.1.0#[method]connection.send",
34            "shonin:agent/inference@0.1.0#infer",
35            "run",
36        ],
37        // The standard WASI packages the world imports are generated here
38        // too, so an agent needs nothing beyond this crate to use them.
39        generate_all,
40        pub_export_macro: true,
41        default_bindings_module: "strategy_sdk::bindings",
42    });
43}
44
45#[cfg(feature = "hyperliquid")]
46pub mod hyperliquid;
47#[cfg(feature = "lighter")]
48pub mod lighter;
49pub mod local;
50#[cfg(feature = "orca")]
51pub mod orca;
52pub mod payload;
53#[cfg(feature = "robinhood")]
54pub mod robinhood;
55pub mod sizing;
56pub mod state;
57pub mod streams;
58
59pub use bindings::Guest as Agent;
60pub use bindings::shonin::agent::types::Verdict;
61pub use bindings::shonin::agent::{
62    data, drivers, inference, secrets, transfers, types, vault, websocket,
63};
64/// Requests to any host, outbound only, if the manifest declares `network`:
65/// the standard `wasi:http` interfaces.
66pub use bindings::wasi::http;
67/// The agent's state: the standard `wasi:keyvalue` interfaces, 64 MiB,
68/// empty every time the agent starts. [`state`] wraps them for the common
69/// case.
70pub use bindings::wasi::keyvalue;
71pub use local::{
72    AccountEvent, AccountState, Action, MarketState, Order, PositionState, Query, Receipt, Side,
73};
74pub use payload::{Decimal, decimal, from_payload, millionths, to_payload};
75pub use sizing::{Limits, size_for};
76
77/// This crate's version. The build service compiles every agent against the
78/// SDK it carries, and takes a lock naming the SDK from crates.io at this
79/// version or an earlier one compatible with it.
80pub const VERSION: &str = env!("CARGO_PKG_VERSION");
81
82/// Declare the agent the host should run.
83///
84/// ```ignore
85/// struct MyAgent;
86/// impl strategy_sdk::Agent for MyAgent {
87///     async fn run() -> Result<(), String> { Ok(()) }
88/// }
89/// strategy_sdk::export_agent!(MyAgent);
90/// ```
91#[macro_export]
92macro_rules! export_agent {
93    ($name:ident) => {
94        $crate::bindings::export!($name with_types_in $crate::bindings);
95    };
96}
97
98impl bindings::shonin::agent::vault::Claim {
99    /// What [`Claim::fraction`](bindings::shonin::agent::vault::Claim) is
100    /// out of: 10^18 is the whole vault.
101    pub const WHOLE: u64 = 1_000_000_000_000_000_000;
102
103    /// The fraction of every position being closed for the claim, as a
104    /// decimal between 0 and 1.
105    pub fn share(&self) -> Decimal {
106        Decimal::from(self.fraction) / Decimal::from(Self::WHOLE)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use bindings::shonin::agent::vault::Claim;
114
115    #[test]
116    fn a_claim_s_fraction_reads_as_a_share_of_the_whole_vault() {
117        let quarter = Claim {
118            id: 3,
119            fraction: Claim::WHOLE / 4,
120        };
121        assert_eq!(quarter.share(), decimal("0.25"));
122        let whole = Claim {
123            id: 4,
124            fraction: Claim::WHOLE,
125        };
126        assert_eq!(whole.share(), decimal("1"));
127    }
128
129    #[test]
130    fn a_feed_and_its_values_read_back_as_they_were_written() {
131        use streams::{Book, HyperliquidFeed, HyperliquidValue, Level};
132        let feed = HyperliquidFeed::Book {
133            coin: "BTC".to_string(),
134        };
135        assert_eq!(
136            from_payload::<HyperliquidFeed>(&to_payload(&feed)),
137            Ok(feed)
138        );
139        let value = HyperliquidValue::Book(Book {
140            market: "BTC".to_string(),
141            bids: vec![Level {
142                price: decimal("64000.5"),
143                size: decimal("0.1"),
144                orders: Some(2),
145            }],
146            asks: Vec::new(),
147        });
148        assert_eq!(
149            from_payload::<HyperliquidValue>(&to_payload(&value)),
150            Ok(value)
151        );
152    }
153}