iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
😽

21 Relatable Experiences in Rust-Based Crypto Bot Development for Botters

に公開

Introduction

Hello, crypto botters!

This is the article for Day 2 of the Crypto Botter Advent Calendar 2024!

Yesterday's articles were Starting Atomic Arbitrage Gently by 5000e12, and Until Atomic Arb on the SUI Chain by shun_m2e.

Both articles explained Atomic Arb from different angles and were very helpful ✨

In this article, as someone currently developing a high-frequency trading bot for bitbank in Rust—that cool programming language—I will introduce the charm of Rust and some common "tropes" that represent its usability.

Even if you're a Botter with no Rust experience, this article might help you feel more familiar with it, and you might even want to start writing your bots in Rust!

Let's become a Rustacean! 🦀

The "What Python Users Might Think" Edition

1. Often thought: "You can't do machine learning in Rust"

Probably about half of the people developing bots in Python choose it because of its rich ecosystem of machine learning libraries, right?

I have good news for you! You can run models created in Python in Rust too!

I'll skip the details, but by converting a model to a general format called ONNX[1] and running it using a Rust wrapper for the ONNX Runtime (ort: https://github.com/pykeio/ort), you should be able to run most machine learning models in Rust.

My development approach is to train decision tree models in Python and then execute those models in Rust!

2. Often thought: "It's a pain to write types every time"

Since Rust is a statically typed language, some might think you have to write out the type every time you define a variable.
However, in addition to Rust's type inference, in modern environments like VSCode, you can use a tool called rust-analyzer to see the results of that type inference. This allows for programming where you "use types without writing them."


The state of rust-analyzer at work (showing that the type of limit_info_buys is inferred as Vec<LimitInfo>)

The "Things I Appreciate About Rust" Edition

3. Truly grateful for being able to lazily print anything by deriving Debug with macros

Many of you have probably been confused when you tried to print a complex class in Python to see its contents, only to get an output like <__main__.HogeClass object at 0x...>, right?

The information you really want to see is that self.a contains 10, etc., but it just tells you the object's address.

In Rust, even complex structs and Enums can be easily output just by adding #[derive(Debug)] (thanks to the mysterious power of procedural macros).

By adding just one line of #[derive(Debug)], even such complex structs can be output for debugging using println!{:?} and similar.

4. Option<T> and Result<T,E> are pretty great

Rust has a type called Enum (enumerated type), which is defined by listing all possible values.

Option and Result are types of this Enum. Option<T> consists only of Some(T) and None[2], and is often used to represent cases where data exists or doesn't exist.

Result<T,E> consists of Ok(T) and Err(E), and is often used to represent things that might not succeed, such as responses from exchanges.

5. Helpful for knowing where you are swallowing errors

To get the equivalent of T in the Option<T> or Result<T,E> mentioned above, you must either call unwrap() while being prepared for a panic if T doesn't exist (if it was None for Option, or Err for Result), or handle the cases using pattern matching.


As shown in the image, an error occurs because addition is not defined for Option<i32> and the integer i32. In the definition of piyo on line 34, you can see that the programmer is confident the contents of the Option are Some because they are adding it after unwraping.

At first glance, this constraint might seem redundant, just forcing you to write unwrap even in situations where it's natural for data to exist. However, it has the benefit of expressing the programmer's responsibility and resolve as unwrap.


A commit that properly handles a situation where unwrap() was being called even when an Err could return due to cutting corners.
It makes you realize where you were lazy or skipped dealing with errors, which often helps you recognize your own shortcomings as a human.

6. Grateful every day for the fact that library documentation is (tends to be) solid

It's so helpful.

7. Grateful for the compiler and editor pointing out errors in advance

When using dynamic typing languages like Python, it was a daily occurrence to fall into despair because of a Type Error in a completely unexpected place. But since Rust is a statically typed language, these kinds of errors can be detected before execution!

The "Things I'm Not So Happy About Rust" Edition

8. Often troubled by compilation taking several minutes

It's just too long. In my environment, it finishes in about a minute, but I've heard it can take 5 or 6 minutes in some cases.

9. Often ends up being extremely heavy as a Docker container image

If you don't put in some effort, the image can end up being around 2GiB. It's a problem. (If you use multi-stage builds properly, it gets down to about 120MB.)

10. A bit troubled by the lack of an official GCP SDK

There are crates developed by the community, so I use those, but I really wish there was an official SDK. Are you listening, Google?


There is no Google Cloud SDK for Rust

11. Often a lack of libraries

(What kind of phrasing is this??)

In languages like Python, there are plenty of APIs for crypto exchanges, but Rust isn't quite as well-equipped in comparison.

However, believe it or not, there's a Rust library that supports API trading on bitbank!?

https://github.com/Harui-i/bitbankutil_rs/tree/main

If this library I wrote helps you, please support me by giving the GitHub repository a star! Your "Likes" motivate my development!

Common Tropes When Just Starting Rust

12. Often the case: "The Rust Programming Language" is actually long

The Rust Programming Language (TRPL) is known as the go-to tutorial for Rust (often called "the book"), but it's actually quite long.

Each chapter is fairly dense, and with 20 chapters in total, I gave up many times.

I worked through the entire TRPL during my summer vacation, and it took about 15 days...
Since I was doing it for several hours a day, I probably spent a total of about 30 to 45 hours, wouldn't you say?

Pure "Tropes"

13. Often failing to understand lifetimes

Rust has a concept called lifetimes, which involves considering how long a reference remains valid. In certain situations, you must specify the lifetimes of variables yourself. However, this is very difficult and often results in getting scolded by the compiler.

14. Often avoiding &str for now because lifetimes are confusing

As a result of how lifetimes, references, and other elements interact, you often find yourself wanting to handle String instead of &str.

15. Often few situations where Rust's speed truly shines

You probably have the impression that Rust is a language as fast as C++, but in reality, there are not many situations in CEX trading where you can feel Rust's speed. Since communicating with an exchange takes tens to hundreds of milliseconds regardless, the impact of Rust's processing speed is not that significant. (Though the story might change for strategies that perform many CPU-bound calculations.)

16. Code tends to get long

I've started to feel like this isn't necessarily a unique characteristic of Rust. Perhaps it's just a common trope for languages with type annotations.
In my experience, bot programs written in Rust actually do tend to become long.


This is the process for calling an exchange API to place an order, but a single statement has become incredibly long thanks to the type annotations.

17. The success rate of code completion by Copilot and AI assistants tends to be clearly lower

Compared to my experience with other languages, I have a sense that the quality of code from generative AI clearly worsens in Rust when asynchronous programming is involved or when logic gets a bit complex. Is it because there isn't enough training data for Rust? Or is it because asynchronous programming itself is inherently difficult?

18. Asynchronous programming tends to be difficult

To begin with, asynchronous programming itself is difficult.
Unlike normal synchronous programming, the order of execution isn't necessarily the same as the order in which the code is written, and you also need to understand Futures and the tokio runtime that handles them.

19. Channels, used after recognizing the inherent difficulty of memory sharing, tend to be revolutionary

Sharing state in asynchronous programming is difficult, and if you want to share a type T, you often have to go through the trouble of using Arc<Mutex<T>>. However, you are often surprised at how cleanly you can write your code using a channel.

For example, the logic for passing data through a channel every time a WebSocket message is received can be written like this! It's clean! (By my standards)


Sending data through a channel once it's received


Processing data sent from the channel

20. Often jealous of Go, which is rumored to handle these asynchronous difficulties easily

Rumor has it that Go doesn't have the various difficult elements of Rust's asynchronous programming (like lifetimes) and that the language handles it all behind the scenes. It's quite envious.

21. The feeling of "I'm so cool for handling a difficult language like Rust" often outweighs the feeling of "I like Rust's robust memory management and high performance"

Rather than just a robust language, there's something cooler about yourself when you're programming while taming a robust and difficult language.

Conclusion

In this article, I introduced some common Rust tropes for Pythonistas and Rustaceans, based on my experience developing a high-frequency trading bot for bitbank in Rust. Was there anything you could relate to?

Rust is a difficult language, but that difficulty is often just a reflection of the difficulty of the subject matter itself. I hope this article has piqued your interest in bot development with Rust, even if just a little.

If you enjoyed this, I'd appreciate it if you could give this article a "Like"! I look forward to any comments, including questions, impressions, or corrections!

脚注
  1. It's apparently pronounced "Onyx," not "O-nonox." ↩︎

  2. Some(T) means that the type associated with Some is T. ↩︎

Discussion