Skip to main content

percli_core/agent/
feed.rs

1use serde::Deserialize;
2use std::io::BufRead;
3use std::path::PathBuf;
4
5/// A single price observation in the feed.
6#[derive(Debug, Clone, Deserialize)]
7pub struct PriceTick {
8    pub oracle: u64,
9    pub slot: u64,
10}
11
12/// Source of price data for the agent tick loop.
13#[derive(Debug, Clone, Deserialize)]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum FeedConfig {
16    Inline { prices: Vec<PriceTick> },
17    Csv { path: PathBuf },
18    Stdin,
19}
20
21impl Default for FeedConfig {
22    fn default() -> Self {
23        Self::Inline {
24            prices: vec![PriceTick {
25                oracle: 1000,
26                slot: 100,
27            }],
28        }
29    }
30}
31
32impl FeedConfig {
33    /// Convert the feed config into a boxed iterator of price ticks.
34    ///
35    /// For CSV and stdin feeds, parse errors are silently skipped.
36    pub fn into_tick_iter(self) -> anyhow::Result<Box<dyn Iterator<Item = PriceTick>>> {
37        match self {
38            FeedConfig::Inline { prices } => Ok(Box::new(prices.into_iter())),
39            FeedConfig::Csv { path } => {
40                let content = std::fs::read_to_string(&path).map_err(|e| {
41                    anyhow::anyhow!("failed to read feed CSV {}: {}", path.display(), e)
42                })?;
43                let ticks: Vec<PriceTick> = content
44                    .lines()
45                    .filter(|line| !line.starts_with('#') && !line.is_empty())
46                    .filter_map(|line| {
47                        let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
48                        if parts.len() >= 2 {
49                            let oracle = parts[0].parse().ok()?;
50                            let slot = parts[1].parse().ok()?;
51                            Some(PriceTick { oracle, slot })
52                        } else {
53                            None
54                        }
55                    })
56                    .collect();
57                Ok(Box::new(ticks.into_iter()))
58            }
59            FeedConfig::Stdin => {
60                let reader = std::io::stdin().lock();
61                let ticks: Vec<PriceTick> = reader
62                    .lines()
63                    .map_while(Result::ok)
64                    .filter(|line| !line.trim().is_empty())
65                    .filter_map(|line| serde_json::from_str(&line).ok())
66                    .collect();
67                Ok(Box::new(ticks.into_iter()))
68            }
69        }
70    }
71}