如何构建自己的量化程序助手

120 阅读11分钟

手把手教你构建自己的量化程序助手

Hey,大家好,今天来一期coding.

在我们部署量化策略程序的时候,不知道你有没有遇到这样的情况,在白天忙了一整天把策略写好的时候部署到服务器, 观察一下午运行良好收益也还不错,于是你便不管这个策略了,让它全天24小时运行吧,但是到了半夜你辗转反侧睡不着,突然想起今天刚上线了一个策略,白天看收益还不错,于是你打开交易软件想查看当前收益情况。糟糕^~^ 策略这时候开始亏损了,你越想越不对,回忆白天写的内容,灵光一现,啊!有bug!!!这时候你的电脑在公司,是不是就要大老远跑回公司呀?难道让它亏损达到停机的阈值,自动停机吗?今天我教你一个实用办法——使用Telegram机器人构建自己的量化程序助手。

前期准备

Telegram 可以在官方网站直接下载安装:Telegram Messenger,这里为了方便教程使用,我使用的pc端的,移动端的操作也类似的。

创建Telegram机器人

下载安装好Telegram后,可以直接使用大陆的手机号码直接注册,流程很简单这里就不一一演示了 注册完登入,直接在搜索框输入:@BotFather,在搜索出来的内容点击进入对话框,然后在对话框输入/newbot并发送。/newbot是创建机器人的意思,然后根据提示,给创建的机器人起一个名字(注意机器人的名字一定要以 _bot为后缀名),大致如下所示:

1748418638069.png

这里它给我们一个HTTP API,请注意它的值,稍后我们会在代码中用到,在此之前我们先给机器人创建一个群组:在设置页面找到New Group创建一个群组,点击并给群组起一个名字,然后添加新成员页面搜索中输入你给机器人起的名字,点击它并选择你的机器人,然后点击Create,它就会创建了一个群组出来了,然后我们点击群组的名称,会打开一个页面,页面中目前就两个人,你和你的机器人,点开右上角...,然后点击Manage group在打开的页面中,选择Adminstrators,在打开的页面中选择Add new admins添加管理员,并将你的机器人设置为管理员并保存,以确保它有足够的权限,方便我们后续代码的开发。

开始Coding

接下来就是激动人心的编码时间了,Telegram社区和第三方提供了很多与Telegram交互的sdk,在这里我以rust为例,教你构建自己的程序助手(当然你也可以使用自己熟悉的语言)

创建新的rust项目
root@DESKTOP-JA7LOFK:~/code$ cargo new quant_grss
 Creating binary (application) `quant_grss`package note: see more`Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
root@DESKTOP-JA7LOFK:~/code/quant_grss$ tree
.
├── Cargo.toml
└── src
└── main.rs

新创建的项目中就一个Hello World 程序,我们需要在项目中添加几个create方便我们开发,整个项目的Cargo.toml的配置如下:

[package]
name = "quant_grss"
version = "0.1.0"
edition = "2024"

[dependencies]
tokio = { version = "1.45.1", features = ["full"] }
teloxide = { version = "0.15.0", features = ["macros"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenv = "0.15.0"
tracing-subscriber = "0.3.19"
tracing = "0.1.41"
async-channel = "2.3.1"
eyre = "0.6.12"

在添加完crate后,我们在项目的根目录下建立一个名为.env文件,并写入格式key=value的内容,key名可以随便写,待会编码的时候你知道就行,value必须是我们在创建机器人是的HTTP API的值,大致如下:

root@DESKTOP-JA7LOFK:~/code/quant_grss$ touch .env
root@DESKTOP-JA7LOFK:~/code/quant_grss$ cat .env
Bot_Token = "7290118000:AAHSK3nyCxEOtxn0_wcISHKTzSLh2xxxxx"

接下来就是回到我们的main.rs文件开始编写代码了.

先来一个简单程序

我将通过一个小小的例子介绍如何与Telegram进行交互,为了mod的统一性,我在src目录下创建了一个grss_bot.rslib.rs的文件,我会在这个文件封装一些与Telegram交互的功能函数,接下来我们来编写我们的第一个功能吧

touch src/lib.rs
touch src/grss_bot.rs

在lib.rs文件中写入如下内容cat src/lib.rs

pub mod grss_bot;

grss_bot.rs编写获取机器人所在群组的id,emm...下面每一行代码我都尽可能的解释了

use teloxide::{prelude::*, utils::command::{self, BotCommands}};
use tracing::{error, info, warn};

// 使用teloxide的BotCommands属性宏定义Telegram的命令,就如同我们在创建机器人是输入的: /newbot 一样
//这个宏定义命令的名称,我们这里使用全小写字母
#[derive(BotCommands, Clone)]
#[command(
    rename_rule = "lowercase",
    description = "These commands are supported:"
)]
pub enum Command { //定义一个枚举值来管理我们的命令
    //定义一个获取群组id的命令
    #[command(description = "display this chat id.")]
    GetChatId(String)
}
// 编写命令的逻辑函数,也就是定义每个命令该完成什么功能
//参数bot:机器人实例,稍后我会把它new出来,
//参数msg:消息实例,包含了Telegram的消息内容,包括用户信息、群组中的信息等等
//参数cmd:命令实例,就是我们自定义的命令、参数等信息
pub async fn init_commands(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {
    match cmd {
        Command::GetChatId(message) => {
            info!("reviced chat id message: {}", message);
            let resp_message = format!("Your message is @{message}.\nchat id is [{}]", msg.chat.id);
            bot.send_message(
                msg.chat.id,
                resp_message,
            )
            .await?;
        }
  
    };
    Ok(())
}

// 编写单元测试,验证我们的命令是否符合逻辑
mod tests {
    use teloxide::prelude::*;
    use tracing::info;
    use tracing_subscriber::fmt;
    use crate::grss_bot::{Command, init_commands};
    #[tokio::test]
    async fn test_bot_command_get_chat_id() {
        //初始化一些环境变量,即.env文件内容中的key-value对
        dotenv::dotenv().ok();
        //初始化日志,方便我们直观的验证程序的正确性
        fmt().pretty().init();
        //获取.env中,key为Bot_Token的值
        let bot_token = dotenv::var("Bot_Token").expect("Bot_Token not found");
        //创建一个机器人实例,使用我们的HTTP API token
        let bot = Bot::new(bot_token);
        //监听并处理接收的命令
        info!("Start listening for commands");
        Command::repl(bot, init_commands).await;
    }
  
}

运行tests中的测试用例,在Chat中我们输入/getchatid + 任意内容就收到了代码中定义机器人回复的消息了,如下在我们的程序端收到这样的消息: 1748486430543.png 同时Chat中也会收到我们写的回复消息:

1748426958669.png

与Telegram交互的教程就这么朴实无华,大道至简复杂的代码都是人为来写的,介绍完如何与Telegram交互后,就到我们的正主了,构建量化程序助手。

构建量化程序助手

封装通用组件

老规矩,为了方便,我将封装一个通用的机器人组件,封装的代码如下,具体功能我将在构建机器人助手时介绍:

#[derive(Debug, Clone)]
pub struct GrssBot {
    bot: Arc<Bot>,
    chat_id: i64,
    rx: Receiver<String>,
    tx: Sender<String>,
    message: String,
    buffer: Arc<Mutex<Vec<String>>>,
    exit_tx: Sender<()>,
    exit_rx: Receiver<()>, 
}
impl GrssBot {
    /// bot 机器人客户端,chat_id 默认接收消息的群,channel_size通道大小
    pub fn new(bot: Bot, chat_id: i64, channel_size: usize) -> Self {
        let (tx, rx): (Sender<String>, Receiver<String>) = bounded(channel_size);
        // 退出信号通道
        let (exit_tx, exit_rx): (Sender<()>, Receiver<()>) = bounded(1); 
        GrssBot {
            bot: Arc::new(bot),
            chat_id,
            rx,
            tx,
            message: String::new(),
            buffer: Arc::new(Mutex::new(Vec::new())),
            exit_tx,
            exit_rx,
        }
    }
    // 发送消息搭配缓冲区暂存
    pub async fn send_message(&self, message: String) -> Result<()> {
        match self.tx.send(message).await {
            Ok(_) => Ok(()),
            Err(err) => {
                warn!("send message error:{}", err);
                return Err(eyre!("send message error||err:{}", err));
            }
        }
    }
    // 直接发送消息到指定的群组
    pub async fn send_message_to_chat_id(&self, chat_id: i64, message: String) -> Result<()> {
        match self
            .bot
            .send_message(ChatId(chat_id), message.clone())
            .await
        {
            Ok(_) => {}
            Err(err) => {
                warn!(
                    file = true,
                    "send message to chat_id error||chat_id:{},message:{}||err={}",
                    chat_id,
                    message,
                    err
                )
            }
        };
        Ok(())
    }
    pub async fn stop(&self) -> Result<()> {
        match self.exit_tx.send(()).await {
            Ok(_) => {}
            Err(err) => {
                error!(file = true, "stop grss bot err|err={}", err)
            }
        }
        Ok(())
    }
    pub async fn run(&mut self) -> Result<()> {
        let mut ping_interval = tokio::time::interval(tokio::time::Duration::from_secs(3));
        loop {
            tokio::select! {
                //接收消息并缓存
                cmd = self.rx.recv() => {
                    let msg= match cmd {
                        Ok(msg) => msg,
                        Err(err) => {
                            warn!(file=true,"receive message from grss error||err={}",err);
                            continue;
                        },
                    };
                    self.buffer.lock().await.push(msg);
                }
                // 接收退出信号
                exit_signal = self.exit_rx.recv() => {
                    match exit_signal {
                        Ok(_) => {
                            //刷新消息然后退出
                            let mut buffer = self.buffer.lock().await;
                            while !buffer.is_empty(){
                                let mut index_to_keep = 0;
                                for (index,msg) in buffer.iter().enumerate() {
                                    self.message +=format!("\n{msg}\n----").as_str();
                                    if self.message.len()>2048||index==buffer.len()-1{
                                            match self.bot.send_message(ChatId(self.chat_id),self.message.clone()).await {
                                                Ok(_) => {},
                                                Err(err) => {
                                                    warn!(file=true,"send message to chat_id error||chat_id:{},message:{}||err={}",self.chat_id,self.message,err)
                                                },
                                            };
                                        self.message.clear();
                                        index_to_keep = index;
                                        break;
                                    }
                                }
                                buffer.drain(..=index_to_keep);
                                tokio::time::sleep(std::time::Duration::from_secs(3)).await;
                            }
                            return Ok(()); 
                        }
                        Err(e) => {
                            return Err(eyre!("Exit channel closed unexpectedly: {}", e));
                        }
                    }
                }
                //定时推送消息
                _ = ping_interval.tick() => {
                    let mut buffer = self.buffer.lock().await;
                    if !buffer.is_empty(){
                        let mut index_to_keep = 0;
                        for (index,msg) in buffer.iter().enumerate() {
                            self.message +=format!("\n{msg}\n----").as_str();
                            if self.message.len()>2048||index==buffer.len()-1{
                                    //这一行注释 解开了之后就报错了
                                    match self.bot.send_message(ChatId(self.chat_id),self.message.clone()).await {
                                        Ok(_) => {},
                                        Err(err) => {
                                            warn!(file=true,"send message to chat_id error||chat_id:{},message:{}||err={}",self.chat_id,self.message,err)
                                        },
                                    };
                                self.message.clear();
                                index_to_keep = index;
                                break;
                            }
                        }
                        buffer.drain(..=index_to_keep);
                    }
                }
            }
        }
    }
}

GrssBot::new 创建机器人组件

GrssBot::send_message 其实teloxide这个crate已经提供了将消息发送给Telegram的功能,这里封装一遍的原因是,经过实测,机器客户端每3s发送一次消息且不能大于4096个字符Telegram才能稳定的接收到,不然会很大概率发送失败,所以我把消息整合起来定期发送;

GrssBot::senf_message_to_chat_id 直接发送消息到Telegram不用缓存,是为了处理在量化程序运行的过程的有一些紧急的消息需要立即通知

GrssBot::runGrssBot::stop 顾名思义就起启动和停止机器人的方法了,注意这里和后面停止和启动策略程序的命令是不一样的

Ok,组件写好了就验证它的逻辑是否符合我们的要求:

    #[tokio::test]
    async fn test_grss_bot() {
        dotenv::dotenv().ok();
        fmt().pretty().init();
        let bot_token = dotenv::var("Bot_Token").expect("Bot_Token not found");
        //创建一个机器人实例,使用我们的HTTP API token
        let bot = Bot::new(bot_token);
        let bot2 = bot.clone();
        let grss = GrssBot::new(bot, -4961893560, 10);
        let grss1 = grss.clone();
        let mut grss2 = grss.clone();
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        //处理其他消息
        tokio::spawn(async move {
            for i in 0..10 {
                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
                grss1
                    .send_message(format!("message{}", i))
                    .await
                    .expect("send msg error");
            }
        });
        //处理命令式 任务
        tokio::spawn(async move {
            Command::repl(bot2, init_commands).await;
        });
        tokio::spawn(async move {
            grss2.run().await.expect("bot run error");
        });
        //某一时刻通知它退出
        // 发送退出信号
        tokio::time::sleep(std::time::Duration::from_secs(8)).await;
        grss.stop().await.expect("stop grss bot error");
        tokio::time::sleep(std::time::Duration::from_secs(8)).await;
    }

还记得我们前边定义得获取群组id的命令吗?

pub enum Command {
    #[command(description = "display this chat id.")]
    GetChatId(String),
}

我们的程序助手就是通过这种方式实现的,我们的策略就可以类似这样实现了:

// 定义Telegram的命令,就如同我们在创建机器人是输入的: /newbot 一样
//这个宏定义命令的名称,我们这里使用全小写字母
#[derive(BotCommands, Clone)]
#[command(
    rename_rule = "lowercase",
    description = "These commands are supported:"
)]
pub enum Command {
    //定义一个获取群组id的命令
    #[command(description = "display this chat id.")]
    GetChatId(String),
    StartPolt,
    StopPolt,
    GetBalnce,
    //实现其他更多的功能,获取资金消息等
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
    //some Fields
}
use lazy_static::lazy_static;
lazy_static! {
    static ref ENABLE_TRADING: Mutex<bool> = Mutex::new(false);
}
pub async fn start_to_trading() -> bool {
    let mut status = ENABLE_TRADING.lock().await;
    if *status {
        return true;
    }
    *status = true;
    *status
}
pub async fn stop_trading() -> bool {
    let mut status = ENABLE_TRADING.lock().await;
    if !*status {
        return false;
    }
    *status = false;
    *status
}
//这个id就是我们之前获取的
const ROOTCHATID: i64 = -4961893560;
// 编写命令的逻辑函数,也就是定义每个命令该完成什么功能
//参数bot:机器人实例,
//参数msg:消息实例,包含了Telegram的消息内容,包括用户信息、群组中的信息等等
//参数cmd:命令实例,就是我们自定义的命令、参数等信息
pub async fn init_commands(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {
    match cmd {
        Command::GetChatId(message) => {
            bot.send_message(
                msg.chat.id,
                format!("Your message is @{message}.\nchat id is [{}]", msg.chat.id),
            )
            .await?;
        }
        Command::StartPolt => {
            if msg.chat.id == ChatId(ROOTCHATID) {
                bot.send_message(
                    msg.chat.id,
                    format!("start plot is: {}", start_to_trading().await),
                )
                .await?;
            } else {
                bot.send_message(
                    msg.chat.id,
                    format!("your not root user.\nchat_id is [{}]", msg.chat.id),
                )
                .await?;
            }
        }
        Command::StopPolt => {
            if msg.chat.id == ChatId(ROOTCHATID) {
                bot.send_message(
                    msg.chat.id,
                    format!("stop plot is: {}", stop_trading().await),
                )
                .await?;
            } else {
                bot.send_message(
                    msg.chat.id,
                    format!("your not root user.\nchat_id is [{}]", msg.chat.id),
                )
                .await?;
            }
        }
        Command::GetBalnce => {
            bot.send_message(
                msg.chat.id,
                format!("your balnce message.\nchat_id is [{}]", msg.chat.id),
            )
            .await?;
        }
    }
    Ok(())
}
pub struct Polt {
    pub some_config: Config,
    pub grss_bot: GrssBot,
}

impl Polt {
    pub fn new(some_config: Config, grss_bot: GrssBot) -> Self {
        Self {
            some_config,
            grss_bot,
        }
    }
    pub async fn init_exchange_data(&self) {
        //初始化交易所公共的行情数据
        //todo some code
        //在接交易所api时,比如出现异常情况,连接重置这些消息就可以通过Telegram通知我们了
        //这里前边我们的小程序获取群组id就id就派上用场了,出现系统级错误时Telegram通知
        self.grss_bot
            .send_message_to_chat_id(self.grss_bot.chat_id, "some important message".to_string())
            .await
            .unwrap(); //简单例子就不进行错误处理了
    }
    async fn clac_signal(&self) {
        //计算开平仓信号
        //一些开平仓的关键消息,这个消息会很多,也不那么重要,但是又需要知道每一笔订单的详细开平仓数据时,就可以发送到缓存中,然后通过Telegram通知我们了
        self.grss_bot
            .send_message("some open position message".to_string())
            .await
            .unwrap();
    }
    async fn do_order(&self) {
        //执行策略订单,在此我们就可以通过Telegram来控制程序的运行了
        //当然也可以直接编写退出函数,
        // 看需求,这里就不多解释了
        if !*ENABLE_TRADING.lock().await {
            return;
        }
        //call exchange api to place order
    }
    //some other methods
}

在进行单元测试,这是它的测试代码,测试结果的验证点太多了,这里放截图的化太多了就不放了,该组件我自己也会用,所以正确性已经通过验证。以上就是如何使用Telegram 机器人成为我们的量化程序助手的全部过程了。复杂的程序都是各种小功能的叠加,至此相信你就可以构独属于自己的机器人了。

此文章内容由云梦量化科技Rust工程师-尘-创作投稿。