mirror of
https://github.com/RomySaputraSihananda/ares.git
synced 2026-08-23 07:38:12 +00:00
feat: make ares self-contained workspace with bundled domain/mt5-client crates
Vendor domain and mt5-client from hermes into crates/ so the repo can be pushed to GitHub without external path dependencies. Also includes all bug fixes from autonomous session: - impulse SL fix (SL at impulse candle low/high, not zone edge) - pip_size fix for 5-decimal pairs - fill_ok premature cancellation fix - TIMEOUT_CANDLES, MIN_FVG_PIPS, MIN_SL_PIPS env vars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e0e421ed05
commit
a78dd26cf0
@@ -0,0 +1,72 @@
|
||||
use rust_decimal::Decimal;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::serde_helpers;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AccountInfo {
|
||||
pub login: u64,
|
||||
pub leverage: u32,
|
||||
pub trade_allowed: bool,
|
||||
pub trade_expert: bool,
|
||||
pub currency: String,
|
||||
pub currency_digits: u8,
|
||||
pub server: String,
|
||||
pub name: String,
|
||||
pub company: String,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub balance: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub equity: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub profit: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub credit: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub margin: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub margin_free: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub margin_level: Decimal,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserializes_from_api_json() {
|
||||
// JSON real from MT5 bridge /account endpoint (live data)
|
||||
let json = r#"{
|
||||
"login": 415817698,
|
||||
"trade_mode": 0,
|
||||
"leverage": 2000,
|
||||
"limit_orders": 1024,
|
||||
"margin_so_mode": 0,
|
||||
"trade_allowed": true,
|
||||
"trade_expert": true,
|
||||
"margin_mode": 2,
|
||||
"currency_digits": 2,
|
||||
"fifo_close": false,
|
||||
"balance": 5000.0,
|
||||
"credit": 0.0,
|
||||
"profit": 0.0,
|
||||
"equity": 5000.0,
|
||||
"margin": 0.0,
|
||||
"margin_free": 5000.0,
|
||||
"margin_level": 0.0,
|
||||
"margin_so_call": 60.0,
|
||||
"name": "Standard",
|
||||
"server": "Exness-MT5Trial14",
|
||||
"currency": "USD",
|
||||
"company": "Exness Technologies Ltd"
|
||||
}"#;
|
||||
let a: AccountInfo = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(a.login, 415817698);
|
||||
assert_eq!(a.leverage, 2000);
|
||||
assert_eq!(a.balance, "5000".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(a.currency, "USD");
|
||||
assert_eq!(a.server, "Exness-MT5Trial14");
|
||||
assert!(a.trade_allowed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::serde_helpers;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Candle {
|
||||
#[serde(with = "serde_helpers::naive_utc_secs")]
|
||||
pub time: DateTime<Utc>,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub open: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub high: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub low: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub close: Decimal,
|
||||
pub tick_volume: u64,
|
||||
pub spread: i32,
|
||||
pub real_volume: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
|
||||
#[test]
|
||||
fn deserializes_from_api_json() {
|
||||
// JSON real from MT5 bridge /rates/from-pos?symbol=BTCUSDm&timeframe=TIMEFRAME_M5
|
||||
let json = r#"{
|
||||
"time": "2026-06-05T19:05:00",
|
||||
"open": 59374.08,
|
||||
"high": 59490.02,
|
||||
"low": 59225.56,
|
||||
"close": 59377.06,
|
||||
"tick_volume": 1869,
|
||||
"spread": 1008,
|
||||
"real_volume": 0
|
||||
}"#;
|
||||
let c: Candle = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(c.time, chrono::Utc.with_ymd_and_hms(2026, 6, 5, 19, 5, 0).unwrap());
|
||||
assert_eq!(c.open, "59374.08".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(c.high, "59490.02".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(c.low, "59225.56".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(c.close, "59377.06".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(c.tick_volume, 1869);
|
||||
assert_eq!(c.spread, 1008);
|
||||
assert_eq!(c.real_volume, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
mod serde_helpers;
|
||||
|
||||
pub mod account;
|
||||
pub mod candle;
|
||||
pub mod position;
|
||||
pub mod symbol;
|
||||
pub mod tick;
|
||||
pub mod timeframe;
|
||||
|
||||
pub use account::AccountInfo;
|
||||
pub use candle::Candle;
|
||||
pub use position::{Position, Side};
|
||||
pub use symbol::Symbol;
|
||||
pub use tick::Tick;
|
||||
pub use timeframe::Timeframe;
|
||||
@@ -0,0 +1,106 @@
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::serde_helpers;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Side {
|
||||
Long,
|
||||
Short,
|
||||
}
|
||||
|
||||
fn de_side<'de, D: Deserializer<'de>>(de: D) -> Result<Side, D::Error> {
|
||||
match u8::deserialize(de)? {
|
||||
0 => Ok(Side::Long),
|
||||
1 => Ok(Side::Short),
|
||||
n => Err(serde::de::Error::custom(format!("unknown position type: {n}"))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Position {
|
||||
pub ticket: u64,
|
||||
pub symbol: String,
|
||||
#[serde(rename = "type", deserialize_with = "de_side")]
|
||||
pub side: Side,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub volume: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub price_open: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub sl: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub tp: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub price_current: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub swap: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub profit: Decimal,
|
||||
pub comment: String,
|
||||
pub magic: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn position_type_0_maps_to_long() {
|
||||
// MT5 "type": 0 = BUY = Long. Extra fields (time, identifier, reason) are ignored by serde.
|
||||
let json = r#"{
|
||||
"ticket": 123456789,
|
||||
"symbol": "BTCUSDm",
|
||||
"type": 0,
|
||||
"volume": 0.1,
|
||||
"price_open": 59000.0,
|
||||
"sl": 58000.0,
|
||||
"tp": 61000.0,
|
||||
"price_current": 59500.0,
|
||||
"swap": -5.0,
|
||||
"profit": 50.0,
|
||||
"comment": "test",
|
||||
"magic": 0,
|
||||
"time": "2026-06-01T10:00:00",
|
||||
"identifier": 123456789,
|
||||
"reason": 0
|
||||
}"#;
|
||||
let p: Position = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(p.side, Side::Long);
|
||||
assert_eq!(p.ticket, 123456789);
|
||||
assert_eq!(p.symbol, "BTCUSDm");
|
||||
assert_eq!(p.volume, "0.1".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(p.sl, "58000".parse::<rust_decimal::Decimal>().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn position_type_1_maps_to_short() {
|
||||
let json = r#"{
|
||||
"ticket": 999,
|
||||
"symbol": "ETHUSDm",
|
||||
"type": 1,
|
||||
"volume": 0.5,
|
||||
"price_open": 1500.0,
|
||||
"sl": 1600.0,
|
||||
"tp": 1400.0,
|
||||
"price_current": 1490.0,
|
||||
"swap": 0.0,
|
||||
"profit": 5.0,
|
||||
"comment": "",
|
||||
"magic": 42,
|
||||
"time": "2026-06-01T12:00:00",
|
||||
"identifier": 999,
|
||||
"reason": 0
|
||||
}"#;
|
||||
let p: Position = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(p.side, Side::Short);
|
||||
assert_eq!(p.magic, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_serializes_as_lowercase() {
|
||||
assert_eq!(serde_json::to_string(&Side::Long).unwrap(), r#""long""#);
|
||||
assert_eq!(serde_json::to_string(&Side::Short).unwrap(), r#""short""#);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
use serde::Deserializer;
|
||||
|
||||
pub(crate) fn de_decimal<'de, D: Deserializer<'de>>(de: D) -> Result<Decimal, D::Error> {
|
||||
use serde::de::Visitor;
|
||||
struct V;
|
||||
impl<'de> Visitor<'de> for V {
|
||||
type Value = Decimal;
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("a number")
|
||||
}
|
||||
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Decimal, E> {
|
||||
Decimal::try_from(v).map_err(E::custom)
|
||||
}
|
||||
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Decimal, E> {
|
||||
Ok(Decimal::from(v))
|
||||
}
|
||||
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Decimal, E> {
|
||||
Ok(Decimal::from(v))
|
||||
}
|
||||
}
|
||||
de.deserialize_any(V)
|
||||
}
|
||||
|
||||
pub(crate) mod naive_utc_secs {
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
const FMT: &str = "%Y-%m-%dT%H:%M:%S";
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<DateTime<Utc>, D::Error> {
|
||||
let s = String::deserialize(de)?;
|
||||
NaiveDateTime::parse_from_str(&s, FMT)
|
||||
.map(|ndt| ndt.and_utc())
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
pub fn serialize<S: Serializer>(dt: &DateTime<Utc>, se: S) -> Result<S::Ok, S::Error> {
|
||||
se.serialize_str(&dt.format(FMT).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod naive_utc_ms {
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
const FMT_PARSE: &str = "%Y-%m-%dT%H:%M:%S%.f"; // accept any precision when reading
|
||||
const FMT_EMIT: &str = "%Y-%m-%dT%H:%M:%S%.3f"; // always write exactly .NNN
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<DateTime<Utc>, D::Error> {
|
||||
let s = String::deserialize(de)?;
|
||||
NaiveDateTime::parse_from_str(&s, FMT_PARSE)
|
||||
.map(|ndt| ndt.and_utc())
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
pub fn serialize<S: Serializer>(dt: &DateTime<Utc>, se: S) -> Result<S::Ok, S::Error> {
|
||||
se.serialize_str(&dt.format(FMT_EMIT).to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use rust_decimal::Decimal;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::serde_helpers;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Symbol {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub digits: u8,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub point: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub bid: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub ask: Decimal,
|
||||
pub spread: i32,
|
||||
pub spread_float: bool,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub volume_min: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub volume_max: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub volume_step: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub trade_contract_size: Decimal,
|
||||
pub currency_base: String,
|
||||
pub currency_profit: String,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserializes_subset_ignores_extra_fields() {
|
||||
// JSON real from MT5 bridge /symbols — BCHUSDm has 80+ fields, we only capture 15
|
||||
let json = r#"{
|
||||
"name": "BCHUSDm",
|
||||
"description": "Bitcoin Cash vs US Dollar",
|
||||
"digits": 2,
|
||||
"point": 0.01,
|
||||
"bid": 202.51,
|
||||
"ask": 217.51,
|
||||
"spread": 1500,
|
||||
"spread_float": true,
|
||||
"volume_min": 0.1,
|
||||
"volume_max": 20.0,
|
||||
"volume_step": 0.01,
|
||||
"trade_contract_size": 1.0,
|
||||
"currency_base": "BCH",
|
||||
"currency_profit": "USD",
|
||||
"category": "Crypto",
|
||||
"custom": false,
|
||||
"chart_mode": 0,
|
||||
"select": true,
|
||||
"visible": true,
|
||||
"session_deals": 0,
|
||||
"unknown_future_field": "ignored"
|
||||
}"#;
|
||||
let s: Symbol = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(s.name, "BCHUSDm");
|
||||
assert_eq!(s.digits, 2);
|
||||
assert_eq!(s.bid, "202.51".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(s.ask, "217.51".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(s.currency_base, "BCH");
|
||||
assert_eq!(s.category, "Crypto");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::serde_helpers;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Tick {
|
||||
#[serde(with = "serde_helpers::naive_utc_secs")]
|
||||
pub time: DateTime<Utc>,
|
||||
#[serde(with = "serde_helpers::naive_utc_ms")]
|
||||
pub time_msc: DateTime<Utc>,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub bid: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub ask: Decimal,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub last: Decimal,
|
||||
pub volume: u64,
|
||||
pub flags: u32,
|
||||
#[serde(deserialize_with = "serde_helpers::de_decimal")]
|
||||
pub volume_real: Decimal,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
|
||||
#[test]
|
||||
fn deserializes_from_api_json() {
|
||||
// JSON real from MT5 bridge /ticks/from?symbol=BTCUSDm
|
||||
let json = r#"{
|
||||
"time": "2026-06-05T00:00:00",
|
||||
"bid": 63801.02,
|
||||
"ask": 63811.1,
|
||||
"last": 0.0,
|
||||
"volume": 0,
|
||||
"time_msc": "2026-06-05T00:00:00.234000",
|
||||
"flags": 134,
|
||||
"volume_real": 0.0
|
||||
}"#;
|
||||
let t: Tick = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(t.time, chrono::Utc.with_ymd_and_hms(2026, 6, 5, 0, 0, 0).unwrap());
|
||||
assert_eq!(t.bid, "63801.02".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(t.ask, "63811.1".parse::<rust_decimal::Decimal>().unwrap());
|
||||
assert_eq!(t.flags, 134);
|
||||
// time_msc must preserve 234ms
|
||||
assert_eq!(t.time_msc.timestamp_subsec_millis(), 234);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Timeframe {
|
||||
#[serde(rename = "TIMEFRAME_M1")] M1,
|
||||
#[serde(rename = "TIMEFRAME_M2")] M2,
|
||||
#[serde(rename = "TIMEFRAME_M3")] M3,
|
||||
#[serde(rename = "TIMEFRAME_M4")] M4,
|
||||
#[serde(rename = "TIMEFRAME_M5")] M5,
|
||||
#[serde(rename = "TIMEFRAME_M6")] M6,
|
||||
#[serde(rename = "TIMEFRAME_M10")] M10,
|
||||
#[serde(rename = "TIMEFRAME_M12")] M12,
|
||||
#[serde(rename = "TIMEFRAME_M15")] M15,
|
||||
#[serde(rename = "TIMEFRAME_M20")] M20,
|
||||
#[serde(rename = "TIMEFRAME_M30")] M30,
|
||||
#[serde(rename = "TIMEFRAME_H1")] H1,
|
||||
#[serde(rename = "TIMEFRAME_H2")] H2,
|
||||
#[serde(rename = "TIMEFRAME_H3")] H3,
|
||||
#[serde(rename = "TIMEFRAME_H4")] H4,
|
||||
#[serde(rename = "TIMEFRAME_H6")] H6,
|
||||
#[serde(rename = "TIMEFRAME_H8")] H8,
|
||||
#[serde(rename = "TIMEFRAME_H12")] H12,
|
||||
#[serde(rename = "TIMEFRAME_D1")] D1,
|
||||
#[serde(rename = "TIMEFRAME_W1")] W1,
|
||||
#[serde(rename = "TIMEFRAME_MN1")] Mn1,
|
||||
}
|
||||
|
||||
impl Timeframe {
|
||||
pub fn as_api_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::M1 => "TIMEFRAME_M1",
|
||||
Self::M2 => "TIMEFRAME_M2",
|
||||
Self::M3 => "TIMEFRAME_M3",
|
||||
Self::M4 => "TIMEFRAME_M4",
|
||||
Self::M5 => "TIMEFRAME_M5",
|
||||
Self::M6 => "TIMEFRAME_M6",
|
||||
Self::M10 => "TIMEFRAME_M10",
|
||||
Self::M12 => "TIMEFRAME_M12",
|
||||
Self::M15 => "TIMEFRAME_M15",
|
||||
Self::M20 => "TIMEFRAME_M20",
|
||||
Self::M30 => "TIMEFRAME_M30",
|
||||
Self::H1 => "TIMEFRAME_H1",
|
||||
Self::H2 => "TIMEFRAME_H2",
|
||||
Self::H3 => "TIMEFRAME_H3",
|
||||
Self::H4 => "TIMEFRAME_H4",
|
||||
Self::H6 => "TIMEFRAME_H6",
|
||||
Self::H8 => "TIMEFRAME_H8",
|
||||
Self::H12 => "TIMEFRAME_H12",
|
||||
Self::D1 => "TIMEFRAME_D1",
|
||||
Self::W1 => "TIMEFRAME_W1",
|
||||
Self::Mn1 => "TIMEFRAME_MN1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Timeframe {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"M1" => Ok(Self::M1),
|
||||
"M2" => Ok(Self::M2),
|
||||
"M3" => Ok(Self::M3),
|
||||
"M4" => Ok(Self::M4),
|
||||
"M5" => Ok(Self::M5),
|
||||
"M6" => Ok(Self::M6),
|
||||
"M10" => Ok(Self::M10),
|
||||
"M12" => Ok(Self::M12),
|
||||
"M15" => Ok(Self::M15),
|
||||
"M20" => Ok(Self::M20),
|
||||
"M30" => Ok(Self::M30),
|
||||
"H1" => Ok(Self::H1),
|
||||
"H2" => Ok(Self::H2),
|
||||
"H3" => Ok(Self::H3),
|
||||
"H4" => Ok(Self::H4),
|
||||
"H6" => Ok(Self::H6),
|
||||
"H8" => Ok(Self::H8),
|
||||
"H12" => Ok(Self::H12),
|
||||
"D1" => Ok(Self::D1),
|
||||
"W1" => Ok(Self::W1),
|
||||
"MN1" => Ok(Self::Mn1),
|
||||
other => Err(format!("unknown timeframe: {other}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn as_api_str_spot_check() {
|
||||
assert_eq!(Timeframe::M5.as_api_str(), "TIMEFRAME_M5");
|
||||
assert_eq!(Timeframe::H4.as_api_str(), "TIMEFRAME_H4");
|
||||
assert_eq!(Timeframe::D1.as_api_str(), "TIMEFRAME_D1");
|
||||
assert_eq!(Timeframe::Mn1.as_api_str(), "TIMEFRAME_MN1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip() {
|
||||
let tf = Timeframe::H1;
|
||||
let json = serde_json::to_string(&tf).unwrap();
|
||||
assert_eq!(json, r#""TIMEFRAME_H1""#);
|
||||
let back: Timeframe = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, tf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_known_variant() {
|
||||
let tf: Timeframe = "M15".parse().unwrap();
|
||||
assert_eq!(tf, Timeframe::M15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_unknown_returns_err() {
|
||||
let result = "invalid".parse::<Timeframe>();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user