use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum ValueExtractor {
Main,
Channel(ChannelPart),
Macd(MacdPart),
Ichimoku(IchimokuPart),
Double(DoublePart),
Triple(TriplePart),
Candle(CandlePart),
Adaptive(AdaptivePart),
Volatility(VolatilityPart),
StatTest(StatTestPart),
CandleAnatomy(CandleAnatomyPart),
Hilbert(HilbertPart),
Signal,
Flag,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelPart {
Upper,
Middle,
Lower,
Bandwidth, PercentB, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacdPart {
Line,
Signal,
Histogram,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IchimokuPart {
Tenkan,
Kijun,
SenkouA,
SenkouB,
Chikou,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DoublePart {
First,
Second,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriplePart {
First,
Second,
Third,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandlePart {
Open,
High,
Low,
Close,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdaptivePart {
Value,
Period,
Alpha,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VolatilityPart {
Total,
CloseClose,
HighLow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatTestPart {
Statistic,
PValue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandleAnatomyPart {
Body,
UpperWick,
LowerWick,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HilbertPart {
Amplitude,
Phase,
Frequency,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputType {
Line,
Histogram,
Band,
Area,
Dots,
Background,
Cloud,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineStyle {
#[default]
Solid,
Dashed,
Dotted,
DashDot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HistogramStyle {
#[default]
FromBottom,
Centered,
FromTop,
}
#[derive(Debug, Clone)]
pub struct OutputSpec {
pub name: String,
pub display_name: String,
pub output_type: OutputType,
pub default_color: String,
pub default_line_width: f32,
pub default_line_style: LineStyle,
pub value_extractor: ValueExtractor,
pub visible_by_default: bool,
}
impl OutputSpec {
pub fn line(
name: impl Into<String>,
display_name: impl Into<String>,
color: impl Into<String>,
line_width: f32,
extractor: ValueExtractor,
) -> Self {
Self {
name: name.into(),
display_name: display_name.into(),
output_type: OutputType::Line,
default_color: color.into(),
default_line_width: line_width,
default_line_style: LineStyle::Solid,
value_extractor: extractor,
visible_by_default: true,
}
}
pub fn histogram(
name: impl Into<String>,
display_name: impl Into<String>,
color: impl Into<String>,
extractor: ValueExtractor,
) -> Self {
Self {
name: name.into(),
display_name: display_name.into(),
output_type: OutputType::Histogram,
default_color: color.into(),
default_line_width: 1.0,
default_line_style: LineStyle::Solid,
value_extractor: extractor,
visible_by_default: true,
}
}
pub fn band(
name: impl Into<String>,
display_name: impl Into<String>,
fill_color: impl Into<String>,
) -> Self {
Self {
name: name.into(),
display_name: display_name.into(),
output_type: OutputType::Band,
default_color: fill_color.into(),
default_line_width: 1.0,
default_line_style: LineStyle::Solid,
value_extractor: ValueExtractor::Main, visible_by_default: true,
}
}
pub fn area(
name: impl Into<String>,
display_name: impl Into<String>,
fill_color: impl Into<String>,
extractor: ValueExtractor,
) -> Self {
Self {
name: name.into(),
display_name: display_name.into(),
output_type: OutputType::Area,
default_color: fill_color.into(),
default_line_width: 1.0,
default_line_style: LineStyle::Solid,
value_extractor: extractor,
visible_by_default: true,
}
}
pub fn cloud(
name: impl Into<String>,
display_name: impl Into<String>,
fill_color: impl Into<String>,
) -> Self {
Self {
name: name.into(),
display_name: display_name.into(),
output_type: OutputType::Cloud,
default_color: fill_color.into(),
default_line_width: 1.0,
default_line_style: LineStyle::Solid,
value_extractor: ValueExtractor::Main,
visible_by_default: true,
}
}
pub fn with_style(mut self, style: LineStyle) -> Self {
self.default_line_style = style;
self
}
pub fn hidden(mut self) -> Self {
self.visible_by_default = false;
self
}
}
#[derive(Debug, Clone)]
pub struct ReferenceLine {
pub value: f64,
pub color: String,
pub style: LineStyle,
pub label: Option<String>,
}
impl ReferenceLine {
pub fn new(value: f64, color: impl Into<String>) -> Self {
Self {
value,
color: color.into(),
style: LineStyle::Dashed,
label: None,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_style(mut self, style: LineStyle) -> Self {
self.style = style;
self
}
}
#[derive(Debug, Clone)]
pub struct RenderingMetadata {
pub indicator_id: String,
pub overlay: bool,
pub outputs: Vec<OutputSpec>,
pub bounds: Option<(f64, f64)>,
pub zero_baseline: bool,
pub histogram_style: HistogramStyle,
pub reference_lines: Vec<ReferenceLine>,
pub default_height_ratio: f32,
pub precision: u32,
}
impl RenderingMetadata {
pub fn builder(indicator_id: impl Into<String>) -> RenderingMetadataBuilder {
RenderingMetadataBuilder::new(indicator_id)
}
pub fn is_oscillator(&self) -> bool {
self.bounds.is_some() && !self.overlay
}
pub fn uses_zero_baseline(&self) -> bool {
self.zero_baseline
}
}
pub struct RenderingMetadataBuilder {
indicator_id: String,
overlay: bool,
outputs: Vec<OutputSpec>,
bounds: Option<(f64, f64)>,
zero_baseline: bool,
histogram_style: HistogramStyle,
reference_lines: Vec<ReferenceLine>,
default_height_ratio: f32,
precision: u32,
}
impl RenderingMetadataBuilder {
pub fn new(indicator_id: impl Into<String>) -> Self {
Self {
indicator_id: indicator_id.into(),
overlay: false,
outputs: Vec::new(),
bounds: None,
zero_baseline: false,
histogram_style: HistogramStyle::FromBottom,
reference_lines: Vec::new(),
default_height_ratio: 0.15,
precision: 4,
}
}
pub fn overlay(mut self) -> Self {
self.overlay = true;
self
}
pub fn sub_pane(mut self) -> Self {
self.overlay = false;
self
}
pub fn output(mut self, output: OutputSpec) -> Self {
self.outputs.push(output);
self
}
pub fn line_output(
mut self,
name: impl Into<String>,
display_name: impl Into<String>,
color: impl Into<String>,
) -> Self {
self.outputs.push(OutputSpec::line(
name,
display_name,
color,
2.0,
ValueExtractor::Main,
));
self
}
pub fn bounds(mut self, min: f64, max: f64) -> Self {
self.bounds = Some((min, max));
self
}
pub fn zero_baseline(mut self) -> Self {
self.zero_baseline = true;
self
}
pub fn histogram_style(mut self, style: HistogramStyle) -> Self {
self.histogram_style = style;
self
}
pub fn reference_line(mut self, line: ReferenceLine) -> Self {
self.reference_lines.push(line);
self
}
pub fn overbought_oversold(mut self, overbought: f64, oversold: f64) -> Self {
self.reference_lines.push(
ReferenceLine::new(overbought, "#FF5722")
.with_label("Overbought")
);
self.reference_lines.push(
ReferenceLine::new(oversold, "#4CAF50")
.with_label("Oversold")
);
self
}
pub fn height_ratio(mut self, ratio: f32) -> Self {
self.default_height_ratio = ratio;
self
}
pub fn precision(mut self, precision: u32) -> Self {
self.precision = precision;
self
}
pub fn build(self) -> RenderingMetadata {
RenderingMetadata {
indicator_id: self.indicator_id,
overlay: self.overlay,
outputs: self.outputs,
bounds: self.bounds,
zero_baseline: self.zero_baseline,
histogram_style: self.histogram_style,
reference_lines: self.reference_lines,
default_height_ratio: self.default_height_ratio,
precision: self.precision,
}
}
}
impl fmt::Display for OutputType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OutputType::Line => write!(f, "Line"),
OutputType::Histogram => write!(f, "Histogram"),
OutputType::Band => write!(f, "Band"),
OutputType::Area => write!(f, "Area"),
OutputType::Dots => write!(f, "Dots"),
OutputType::Background => write!(f, "Background"),
OutputType::Cloud => write!(f, "Cloud"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rendering_metadata_builder() {
let meta = RenderingMetadata::builder("RSI")
.sub_pane()
.line_output("rsi", "RSI", "#9C27B0")
.bounds(0.0, 100.0)
.overbought_oversold(70.0, 30.0)
.precision(2)
.build();
assert_eq!(meta.indicator_id, "RSI");
assert!(!meta.overlay);
assert_eq!(meta.outputs.len(), 1);
assert_eq!(meta.bounds, Some((0.0, 100.0)));
assert_eq!(meta.reference_lines.len(), 2);
assert!(meta.is_oscillator());
}
#[test]
fn test_output_spec_line() {
let output = OutputSpec::line("sma", "SMA", "#2196F3", 2.0, ValueExtractor::Main);
assert_eq!(output.name, "sma");
assert_eq!(output.output_type, OutputType::Line);
assert_eq!(output.default_color, "#2196F3");
assert!(output.visible_by_default);
}
#[test]
fn test_macd_rendering() {
let meta = RenderingMetadata::builder("MACD")
.sub_pane()
.output(OutputSpec::line("macd", "MACD", "#2196F3", 2.0, ValueExtractor::Macd(MacdPart::Line)))
.output(OutputSpec::line("signal", "Signal", "#FF5722", 1.0, ValueExtractor::Macd(MacdPart::Signal)))
.output(OutputSpec::histogram("histogram", "Histogram", "#4CAF50", ValueExtractor::Macd(MacdPart::Histogram)))
.zero_baseline()
.histogram_style(HistogramStyle::Centered)
.build();
assert_eq!(meta.outputs.len(), 3);
assert!(meta.uses_zero_baseline());
assert_eq!(meta.histogram_style, HistogramStyle::Centered);
}
#[test]
fn test_overlay_indicator() {
let meta = RenderingMetadata::builder("SMA")
.overlay()
.line_output("sma", "SMA", "#2196F3")
.build();
assert!(meta.overlay);
assert!(!meta.is_oscillator());
}
}