Skip to main content

tse_client/
models.rs

1//! Data structures parsed from the TSETMC server, mirroring the JS classes.
2
3use crate::error::{Error, Result};
4use crate::util::clean_fa;
5
6/// Column identifiers, indexed exactly like the JS `cols` array.
7pub const COLS: [&str; 15] = [
8    "date",
9    "dateshamsi",
10    "open",
11    "high",
12    "low",
13    "last",
14    "close",
15    "vol",
16    "count",
17    "value",
18    "yesterday",
19    "symbol",
20    "name",
21    "namelatin",
22    "companycode",
23];
24
25/// Persian column names, indexed exactly like the JS `colsFa` array.
26pub const COLS_FA: [&str; 15] = [
27    "تاریخ میلادی",
28    "تاریخ شمسی",
29    "اولین قیمت",
30    "بیشترین قیمت",
31    "کمترین قیمت",
32    "آخرین قیمت",
33    "قیمت پایانی",
34    "حجم معاملات",
35    "تعداد معاملات",
36    "ارزش معاملات",
37    "قیمت پایانی دیروز",
38    "نماد",
39    "نام",
40    "نام لاتین",
41    "کد شرکت",
42];
43
44/// A single closing-price row. All numeric fields are kept as strings to
45/// preserve the exact textual form (the JS does the same and only converts
46/// when needed), which matters for decimal-accurate price adjustment.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ClosingPrice {
49    pub ins_code: String,        // InsCode (int64)
50    pub deven: String,           // DEven (int32)
51    pub pclosing: String,        // close
52    pub pdr_cot_val: String,     // last
53    pub ztot_tran: String,       // count
54    pub qtot_tran5j: String,     // volume
55    pub qtot_cap: String,        // value
56    pub price_min: String,       // low
57    pub price_max: String,       // high
58    pub price_yesterday: String, // yesterday
59    pub price_first: String,     // open
60}
61
62impl ClosingPrice {
63    /// Parse from a comma-separated row (11 fields).
64    pub fn parse(row: &str) -> Result<Self> {
65        let f: Vec<&str> = row.split(',').collect();
66        if f.len() != 11 {
67            return Err(Error::InvalidData("Invalid ClosingPrice data!".into()));
68        }
69        Ok(ClosingPrice {
70            ins_code: f[0].to_string(),
71            deven: f[1].to_string(),
72            pclosing: f[2].to_string(),
73            pdr_cot_val: f[3].to_string(),
74            ztot_tran: f[4].to_string(),
75            qtot_tran5j: f[5].to_string(),
76            qtot_cap: f[6].to_string(),
77            price_min: f[7].to_string(),
78            price_max: f[8].to_string(),
79            price_yesterday: f[9].to_string(),
80            price_first: f[10].to_string(),
81        })
82    }
83
84    /// Serialize back to a comma-separated row, preserving field order.
85    pub fn to_csv_row(&self) -> String {
86        [
87            &self.ins_code,
88            &self.deven,
89            &self.pclosing,
90            &self.pdr_cot_val,
91            &self.ztot_tran,
92            &self.qtot_tran5j,
93            &self.qtot_cap,
94            &self.price_min,
95            &self.price_max,
96            &self.price_yesterday,
97            &self.price_first,
98        ]
99        .map(|s| s.as_str())
100        .join(",")
101    }
102}
103
104/// A resolved output column (name + Persian name + header).
105#[derive(Debug, Clone)]
106pub struct Column {
107    pub name: &'static str,
108    pub fname: &'static str,
109    pub header: String,
110}
111
112impl Column {
113    /// Build from a column index and optional header override.
114    pub fn new(index: usize, header: Option<String>) -> Result<Self> {
115        if index >= COLS.len() {
116            return Err(Error::InvalidData("Invalid Column data!".into()));
117        }
118        let name = COLS[index];
119        let header = header
120            .filter(|h| !h.is_empty())
121            .unwrap_or_else(|| name.to_string());
122        Ok(Column {
123            name,
124            fname: COLS_FA[index],
125            header,
126        })
127    }
128}
129
130/// An instrument (symbol) descriptor.
131#[derive(Debug, Clone)]
132pub struct Instrument {
133    pub ins_code: String,
134    pub instrument_id: String,
135    pub latin_symbol: String,
136    pub latin_name: String,
137    pub company_code: String,
138    pub symbol: String,
139    pub name: String,
140    pub cisin: String,
141    pub deven: String,
142    pub flow: String,
143    pub lsoc30: String,
144    pub cgds_val: String,
145    pub cgr_val_cot: String,
146    pub ymar_nsc: String,
147    pub ccom_val: String,
148    pub csec_val: String,
149    pub cso_sec_val: String,
150    pub yval: String,
151    pub symbol_original: Option<String>,
152}
153
154impl Instrument {
155    pub fn parse(row: &str) -> Result<Self> {
156        let f: Vec<&str> = row.split(',').collect();
157        if f.len() != 18 && f.len() != 19 {
158            return Err(Error::InvalidData("Invalid Instrument data!".into()));
159        }
160        Ok(Instrument {
161            ins_code: f[0].to_string(),
162            instrument_id: f[1].to_string(),
163            latin_symbol: f[2].to_string(),
164            latin_name: f[3].to_string(),
165            company_code: f[4].to_string(),
166            symbol: clean_fa(f[5]).trim().to_string(),
167            name: f[6].to_string(),
168            cisin: f[7].to_string(),
169            deven: f[8].to_string(),
170            flow: f[9].to_string(),
171            lsoc30: f[10].to_string(),
172            cgds_val: f[11].to_string(),
173            cgr_val_cot: f[12].to_string(),
174            ymar_nsc: f[13].to_string(),
175            ccom_val: f[14].to_string(),
176            csec_val: f[15].to_string(),
177            cso_sec_val: f[16].to_string(),
178            yval: f[17].to_string(),
179            symbol_original: f
180                .get(18)
181                .filter(|s| !s.is_empty())
182                .map(|s| clean_fa(s).trim().to_string()),
183        })
184    }
185}
186
187/// Intraday instrument descriptor.
188#[derive(Debug, Clone)]
189pub struct InstrumentItd {
190    pub ins_code: String,
191    pub lval30: String,
192    pub lval18afc: String,
193    pub flow_title: String,
194    pub cgr_val_cot_title: String,
195    pub flow: String,
196    pub cgr_val_cot: String,
197    pub cisin: String,
198    pub instrument_id: String,
199    pub ztitad: String,
200    pub base_vol: String,
201}
202
203impl InstrumentItd {
204    pub fn parse(row: &str) -> Result<Self> {
205        let f: Vec<&str> = row.split(',').collect();
206        if f.len() != 11 {
207            return Err(Error::InvalidData("Invalid InstrumentITD data!".into()));
208        }
209        Ok(InstrumentItd {
210            ins_code: f[0].to_string(),
211            lval30: clean_fa(f[1]),
212            lval18afc: clean_fa(f[2]),
213            flow_title: clean_fa(f[3]),
214            cgr_val_cot_title: clean_fa(f[4]),
215            flow: f[5].to_string(),
216            cgr_val_cot: f[6].to_string(),
217            cisin: f[7].to_string(),
218            instrument_id: f[8].to_string(),
219            ztitad: f[9].to_string(),
220            base_vol: f[10].to_string(),
221        })
222    }
223}
224
225/// A share-count change record (used for capital-increase adjustments).
226#[derive(Debug, Clone)]
227pub struct Share {
228    pub idn: String,
229    pub ins_code: String,
230    pub deven: String,
231    pub number_of_share_new: i64,
232    pub number_of_share_old: i64,
233}
234
235impl Share {
236    pub fn parse(row: &str) -> Result<Self> {
237        let f: Vec<&str> = row.split(',').collect();
238        if f.len() != 5 {
239            return Err(Error::InvalidData("Invalid Share data!".into()));
240        }
241        Ok(Share {
242            idn: f[0].to_string(),
243            ins_code: f[1].to_string(),
244            deven: f[2].to_string(),
245            number_of_share_new: f[3].trim().parse().unwrap_or(0),
246            number_of_share_old: f[4].trim().parse().unwrap_or(0),
247        })
248    }
249}
250
251/// Intraday group column definitions, mirroring `itdGroupCols`.
252pub fn itd_group_cols() -> Vec<(&'static str, Vec<&'static str>)> {
253    vec![
254        (
255            "price",
256            vec![
257                "time",
258                "last",
259                "close",
260                "open",
261                "high",
262                "low",
263                "count",
264                "volume",
265                "value",
266                "discarded",
267            ],
268        ),
269        (
270            "order",
271            vec![
272                "time", "row", "askcount", "askvol", "askprice", "bidprice", "bidvol", "bidcount",
273            ],
274        ),
275        (
276            "trade",
277            vec!["time", "count", "volume", "price", "discarded"],
278        ),
279        (
280            "client",
281            vec![
282                "pbvol", "pbcount", "pbval", "pbprice", "pbvolpot", "psvol", "pscount", "psval",
283                "psprice", "psvolpot", "lbvol", "lbcount", "lbval", "lbprice", "lbvolpot", "lsvol",
284                "lscount", "lsval", "lsprice", "lsvolpot", "lpchg",
285            ],
286        ),
287        ("misc", vec!["basevol", "flow", "daymin", "daymax", "state"]),
288        (
289            "shareholder",
290            vec![
291                "shares",
292                "sharespot",
293                "change",
294                "companycode",
295                "companyname",
296            ],
297        ),
298    ]
299}