Skip to main content

qs_strategy/
config.rs

1use std::fmt;
2
3use qs_core::OrderType;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::{Literal, ScalarType, ValueType, validate_id};
7
8pub const MAX_SOURCES: usize = 32;
9pub const MAX_COMPLETED_BARS: usize = 32;
10pub const MAX_MATERIALS: usize = 128;
11pub const MAX_MATERIAL_INPUTS: usize = 16;
12pub const MAX_MATERIAL_LOOKBACK: usize = 4096;
13pub const MAX_MATERIAL_STATE_BYTES: usize = 65_536;
14pub const MAX_STATES: usize = 64;
15pub const MAX_VARIABLES: usize = 64;
16pub const MAX_TRANSITIONS: usize = 64;
17pub const MAX_ACTIONS: usize = 16;
18pub const MAX_ASSIGNMENTS: usize = 32;
19pub const MAX_ENTRY_TARGETS: usize = 16;
20pub const MAX_NOTES: usize = 16;
21pub const MAX_NAMED_VALUES: usize = 32;
22pub const MAX_EXPR_NODES: usize = 512;
23pub const MAX_EXPR_DEPTH: usize = 32;
24pub const MAX_LEGS: usize = 16;
25pub const MAX_OUTPUT_COMMANDS: usize = 16;
26pub const MAX_OUTPUT_NOTES: usize = 16;
27pub const MAX_COMMAND_CORRELATIONS: usize = 64;
28pub const MAX_PENDING_FEEDBACK: usize = 64;
29
30/// Bounded logical completed-bar source identity.
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
32#[serde(transparent)]
33pub struct SourceId(String);
34
35impl SourceId {
36    pub fn new(value: impl Into<String>) -> Result<Self, String> {
37        let value = value.into();
38        validate_id(&value)?;
39        Ok(Self(value))
40    }
41
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45}
46
47impl fmt::Display for SourceId {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        formatter.write_str(&self.0)
50    }
51}
52
53impl<'de> Deserialize<'de> for SourceId {
54    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55    where
56        D: Deserializer<'de>,
57    {
58        let value = String::deserialize(deserializer)?;
59        Self::new(value).map_err(serde::de::Error::custom)
60    }
61}
62
63/// Strict configured strategy document.
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct StrategyConfig {
67    pub strategy_id: String,
68    pub title: String,
69    pub initial_state: String,
70    pub sources: Vec<SourceId>,
71    pub trade_slots: Vec<String>,
72    #[serde(default)]
73    pub materials: Vec<MaterialConfig>,
74    #[serde(default)]
75    pub variables: Vec<VariableConfig>,
76    pub states: Vec<StateConfig>,
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct MaterialConfig {
82    pub id: String,
83    pub key: String,
84    #[serde(default)]
85    pub inputs: Vec<Expr>,
86    #[serde(default)]
87    pub params: MaterialParams,
88}
89
90/// Strict built-in material parameters. Custom factories are parameterless.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
92#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
93pub enum MaterialParams {
94    #[default]
95    None,
96    BarField {
97        source: SourceId,
98        field: BarField,
99    },
100    Ema {
101        period: u16,
102    },
103    Atr {
104        source: SourceId,
105        period: u16,
106    },
107    Position {
108        slot: String,
109    },
110    Feedback {
111        slot: String,
112        action: ConfiguredActionKind,
113    },
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum BarField {
119    Open,
120    High,
121    Low,
122    Close,
123    Volume,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum PositionField {
129    Exists,
130    IsPending,
131    IsOpen,
132    EntryPrice,
133    Side,
134    RemainingSize,
135    Stoploss,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "snake_case")]
140pub enum FeedbackField {
141    EntryFilled,
142    EntryRejected,
143    PositionClosed,
144    CancellationApplied,
145    CancellationRejected,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub enum ConfiguredActionKind {
151    Entry,
152    Close,
153    ClosePartial,
154    MoveStoplossToEntry,
155    ModifyStoploss,
156    CancelPending,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum DecisionKind {
162    Entry,
163    Management,
164    Exit,
165    Observation,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum NoteKind {
171    Observation,
172    Risk,
173    Execution,
174    Lifecycle,
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct VariableConfig {
180    pub id: String,
181    pub value_type: ValueType,
182    pub initial: Literal,
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186#[serde(deny_unknown_fields)]
187pub struct StateConfig {
188    pub id: String,
189    #[serde(default)]
190    pub transitions: Vec<TransitionConfig>,
191}
192
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194#[serde(deny_unknown_fields)]
195pub struct TransitionConfig {
196    pub priority: i32,
197    pub target: String,
198    pub when: Expr,
199    #[serde(default)]
200    pub assignments: Vec<AssignmentConfig>,
201    pub decision: Option<DecisionTemplate>,
202    #[serde(default)]
203    pub actions: Vec<ActionTemplate>,
204    #[serde(default)]
205    pub notes: Vec<NoteTemplate>,
206}
207
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209#[serde(deny_unknown_fields)]
210pub struct AssignmentConfig {
211    pub variable: String,
212    pub value: Expr,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct DecisionTemplate {
218    pub kind: DecisionKind,
219    pub reason: String,
220    pub trade_slot: Option<String>,
221    #[serde(default)]
222    pub values: Vec<NamedExpr>,
223}
224
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226#[serde(deny_unknown_fields)]
227pub struct NoteTemplate {
228    pub kind: NoteKind,
229    pub reason: String,
230    pub trade_slot: Option<String>,
231    #[serde(default)]
232    pub values: Vec<NamedExpr>,
233}
234
235#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236#[serde(deny_unknown_fields)]
237pub struct NamedExpr {
238    pub name: String,
239    pub value: Expr,
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
244pub enum Expr {
245    Literal {
246        value: Literal,
247    },
248    Variable {
249        id: String,
250    },
251    Material {
252        id: String,
253    },
254    Input {
255        field: String,
256        value_type: ValueType,
257    },
258    Bar {
259        source: SourceId,
260        field: BarField,
261    },
262    Position {
263        slot: String,
264        field: PositionField,
265    },
266    Feedback {
267        slot: String,
268        action: ConfiguredActionKind,
269        field: FeedbackField,
270    },
271    InputTime,
272    Readiness,
273    Eq {
274        left: Box<Expr>,
275        right: Box<Expr>,
276    },
277    Ne {
278        left: Box<Expr>,
279        right: Box<Expr>,
280    },
281    Lt {
282        left: Box<Expr>,
283        right: Box<Expr>,
284    },
285    Le {
286        left: Box<Expr>,
287        right: Box<Expr>,
288    },
289    Gt {
290        left: Box<Expr>,
291        right: Box<Expr>,
292    },
293    Ge {
294        left: Box<Expr>,
295        right: Box<Expr>,
296    },
297    All {
298        items: Vec<Expr>,
299    },
300    Any {
301        items: Vec<Expr>,
302    },
303    Not {
304        value: Box<Expr>,
305    },
306    Add {
307        left: Box<Expr>,
308        right: Box<Expr>,
309    },
310    Sub {
311        left: Box<Expr>,
312        right: Box<Expr>,
313    },
314    Mul {
315        left: Box<Expr>,
316        right: Box<Expr>,
317    },
318    Div {
319        left: Box<Expr>,
320        right: Box<Expr>,
321    },
322    Min {
323        left: Box<Expr>,
324        right: Box<Expr>,
325    },
326    Max {
327        left: Box<Expr>,
328        right: Box<Expr>,
329    },
330    Abs {
331        value: Box<Expr>,
332    },
333    IsPresent {
334        value: Box<Expr>,
335    },
336    IsMissing {
337        value: Box<Expr>,
338    },
339}
340
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
343pub enum ActionTemplate {
344    Entry {
345        slot: String,
346        side: Expr,
347        order_type: OrderType,
348        price: Expr,
349        risk: Expr,
350        stoploss: Expr,
351        #[serde(default)]
352        targets: Vec<Expr>,
353    },
354    Close {
355        slot: String,
356    },
357    ClosePartial {
358        slot: String,
359        ratio: Expr,
360    },
361    MoveStoplossToEntry {
362        slot: String,
363    },
364    ModifyStoploss {
365        slot: String,
366        price: Expr,
367    },
368    CancelPending {
369        slot: String,
370    },
371}
372
373pub(crate) fn bar_field_type(field: BarField) -> ValueType {
374    match field {
375        BarField::Volume => ValueType::optional(ScalarType::Number),
376        _ => ValueType::optional(ScalarType::Price),
377    }
378}