mirror of
https://github.com/RomySaputraSihananda/ares.git
synced 2026-08-12 18:28:06 +00:00
feat: live trading, multi-pair backtest, README, CI workflow
- Add live trading loop (live.rs) with MT5 pending order placement, state persistence, and per-symbol tokio task for multi-pair - Extract backtest engine to backtest.rs and shared helpers to helpers.rs - Multi-pair support via SYMBOLS env var (comma-separated) - Add GitHub Actions workflow: Linux musl + Windows release binaries - Add README with strategy docs, config reference, backtest results - Clean up warnings, remove unused env vars, tighten .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ddd8c98af5
commit
7d094591b8
@@ -1,7 +1,7 @@
|
||||
use domain::{AccountInfo, Candle, Position, Symbol, Tick, Timeframe};
|
||||
|
||||
use crate::error::Mt5Error;
|
||||
use crate::types::{ApiErrorBody, DataOne, DataVec, HealthStatus, OrderCheckResult, TradeRequest, TradeResult};
|
||||
use crate::types::{ApiErrorBody, DataOne, DataVec, HealthStatus, OrderCheckResult, PendingOrder, TradeRequest, TradeResult};
|
||||
|
||||
pub struct Mt5Client {
|
||||
base_url: String,
|
||||
@@ -87,6 +87,27 @@ impl Mt5Client {
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
pub async fn orders(&self, symbol: &str) -> Result<Vec<PendingOrder>, Mt5Error> {
|
||||
let url = format!("{}/orders", self.base_url);
|
||||
let text = self
|
||||
.fetch_text(self.http.get(&url).query(&[("symbol", symbol)]))
|
||||
.await?;
|
||||
tracing::debug!(endpoint = %url, "mt5 response ok");
|
||||
let w: DataVec<PendingOrder> = serde_json::from_str(&text)?;
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
pub async fn cancel_order(&self, ticket: u64, symbol: &str) -> Result<TradeResult, Mt5Error> {
|
||||
let req = TradeRequest::cancel(symbol, ticket);
|
||||
#[derive(serde::Serialize)]
|
||||
struct Body<'a> { request: &'a TradeRequest }
|
||||
let url = format!("{}/order/send", self.base_url);
|
||||
let text = self.fetch_text(self.http.post(&url).json(&Body { request: &req })).await?;
|
||||
tracing::debug!(endpoint = %url, ticket, "cancel order ok");
|
||||
let w: DataOne<TradeResult> = serde_json::from_str(&text)?;
|
||||
Ok(w.data)
|
||||
}
|
||||
|
||||
pub async fn order_check(&self, request: &TradeRequest) -> Result<OrderCheckResult, Mt5Error> {
|
||||
#[derive(serde::Serialize)]
|
||||
struct Body<'a> {
|
||||
@@ -192,13 +213,15 @@ mod tests {
|
||||
let tr = TradeRequest {
|
||||
action: 1,
|
||||
symbol: "BTCUSDm".into(),
|
||||
volume: 0.01,
|
||||
order_type: 0,
|
||||
price: 60720.0,
|
||||
volume: Some(0.01),
|
||||
order_type: Some(0),
|
||||
price: Some(60720.0),
|
||||
sl: None,
|
||||
tp: None,
|
||||
magic: None,
|
||||
comment: None,
|
||||
order: None,
|
||||
deviation: None,
|
||||
};
|
||||
let json = serde_json::to_string(&tr).unwrap();
|
||||
assert!(json.contains(r#""type":0"#), "order_type must serialize as \"type\"");
|
||||
|
||||
@@ -4,4 +4,4 @@ mod types;
|
||||
|
||||
pub use client::Mt5Client;
|
||||
pub use error::Mt5Error;
|
||||
pub use types::{HealthStatus, OrderCheckResult, TradeRequest, TradeResult};
|
||||
pub use types::{HealthStatus, OrderCheckResult, PendingOrder, TradeRequest, TradeResult};
|
||||
|
||||
@@ -14,10 +14,12 @@ pub struct HealthStatus {
|
||||
pub struct TradeRequest {
|
||||
pub action: u32,
|
||||
pub symbol: String,
|
||||
pub volume: f64,
|
||||
#[serde(rename = "type")]
|
||||
pub order_type: u32,
|
||||
pub price: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub volume: Option<f64>,
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub order_type: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub price: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sl: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -26,6 +28,76 @@ pub struct TradeRequest {
|
||||
pub magic: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
/// Ticket number — required for TRADE_ACTION_REMOVE (cancel pending order)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub order: Option<u64>,
|
||||
/// Allowed price deviation in points
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deviation: Option<u32>,
|
||||
}
|
||||
|
||||
impl TradeRequest {
|
||||
pub fn limit(
|
||||
side: domain::Side,
|
||||
symbol: impl Into<String>,
|
||||
volume: f64,
|
||||
price: f64,
|
||||
sl: f64,
|
||||
tp: f64,
|
||||
magic: u64,
|
||||
comment: impl Into<String>,
|
||||
) -> Self {
|
||||
// TRADE_ACTION_PENDING = 5; BUY_LIMIT = 2, SELL_LIMIT = 3
|
||||
let order_type = match side {
|
||||
domain::Side::Long => 2,
|
||||
domain::Side::Short => 3,
|
||||
};
|
||||
Self {
|
||||
action: 5,
|
||||
symbol: symbol.into(),
|
||||
volume: Some(volume),
|
||||
order_type: Some(order_type),
|
||||
price: Some(price),
|
||||
sl: Some(sl),
|
||||
tp: Some(tp),
|
||||
magic: Some(magic),
|
||||
comment: Some(comment.into()),
|
||||
order: None,
|
||||
deviation: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(symbol: impl Into<String>, ticket: u64) -> Self {
|
||||
// TRADE_ACTION_REMOVE = 8
|
||||
Self {
|
||||
action: 8,
|
||||
symbol: symbol.into(),
|
||||
volume: None,
|
||||
order_type: None,
|
||||
price: None,
|
||||
sl: None,
|
||||
tp: None,
|
||||
magic: None,
|
||||
comment: None,
|
||||
order: Some(ticket),
|
||||
deviation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pending (unfilled) limit order in MT5.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PendingOrder {
|
||||
pub ticket: u64,
|
||||
pub symbol: String,
|
||||
#[serde(rename = "type")]
|
||||
pub order_type: u32,
|
||||
pub volume_initial: f64,
|
||||
pub price_open: f64,
|
||||
pub sl: f64,
|
||||
pub tp: f64,
|
||||
pub magic: u64,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user