use serde_json::{Value, json};
use super::quote_map_to_value;
use crate::ta::types::{
AdxPoint, AnalyzeOutput, AnalyzeSymbolResult, BbandsPoint, DashboardOutput, DerivedFields,
MacdPoint, MomentumIndicators, MomentumSignal, SignalSummary, StochPoint, TaPoint,
TrendIndicators, TrendSignal, VolatilityIndicators, VolatilitySignal, VolumeIndicators,
VolumeSignal,
};
#[test]
fn analyze_output_all_succeed_has_no_errors() {
let output = AnalyzeOutput {
results: vec![successful_result("AAPL")],
};
let result = &output.results[0];
assert!(result.quote.is_some());
assert!(result.analysis.is_some());
assert!(result.quote_error.is_none());
assert!(result.analysis_error.is_none());
}
#[test]
fn analyze_output_quote_fails_dashboard_succeeds_keeps_analysis() {
let output = AnalyzeOutput {
results: vec![quote_failed_result("AAPL")],
};
let result = &output.results[0];
assert!(result.quote.is_none());
assert!(result.analysis.is_some());
assert_eq!(result.quote_error.as_deref(), Some("quote timeout"));
assert!(result.analysis_error.is_none());
}
#[test]
fn analyze_output_dashboard_fails_quote_succeeds_keeps_quote() {
let output = AnalyzeOutput {
results: vec![analysis_failed_result("AAPL")],
};
let result = &output.results[0];
assert!(result.quote.is_some());
assert!(result.analysis.is_none());
assert!(result.quote_error.is_none());
assert_eq!(result.analysis_error.as_deref(), Some("not enough candles"));
}
#[test]
fn analyze_output_single_symbol_both_fail() {
let output = AnalyzeOutput {
results: vec![failed_result("AAPL")],
};
let result = &output.results[0];
assert!(result.quote.is_none());
assert!(result.analysis.is_none());
assert_eq!(result.quote_error.as_deref(), Some("quote timeout"));
assert_eq!(result.analysis_error.as_deref(), Some("not enough candles"));
}
#[test]
fn analyze_output_multi_symbol_mixed_results() {
let output = AnalyzeOutput {
results: vec![
quote_failed_result("AAPL"),
failed_result("MSFT"),
successful_result("SPY"),
],
};
assert_eq!(output.results.len(), 3);
assert!(output.results[0].analysis.is_some());
assert!(output.results[0].quote.is_none());
assert!(output.results[1].quote.is_none());
assert!(output.results[1].analysis.is_none());
assert!(output.results[2].quote.is_some());
assert!(output.results[2].analysis.is_some());
}
#[test]
fn analyze_output_multi_symbol_all_completely_fail() {
let output = AnalyzeOutput {
results: vec![failed_result("AAPL"), failed_result("MSFT")],
};
assert_eq!(output.results.len(), 2);
assert!(
output
.results
.iter()
.all(|result| result.quote.is_none() && result.analysis.is_none())
);
}
#[test]
fn analyze_output_serializes_to_json() {
let output = AnalyzeOutput {
results: vec![successful_result("AAPL")],
};
let value = serde_json::to_value(&output).unwrap();
assert!(value["results"].is_array());
assert_eq!(value["results"][0]["symbol"], "AAPL");
assert!(value["results"][0]["quote"].is_object());
assert!(value["results"][0]["analysis"].is_object());
}
#[test]
fn quote_map_to_value_serializes_empty_quotes() {
let value = quote_map_to_value(Default::default()).unwrap();
assert_eq!(value, json!([]));
}
fn successful_result(symbol: &str) -> AnalyzeSymbolResult {
result(
symbol,
Some(quote_value(symbol)),
Some(sample_dashboard(symbol)),
None,
None,
)
}
fn quote_failed_result(symbol: &str) -> AnalyzeSymbolResult {
result(
symbol,
None,
Some(sample_dashboard(symbol)),
Some("quote timeout"),
None,
)
}
fn analysis_failed_result(symbol: &str) -> AnalyzeSymbolResult {
result(
symbol,
Some(quote_value(symbol)),
None,
None,
Some("not enough candles"),
)
}
fn failed_result(symbol: &str) -> AnalyzeSymbolResult {
result(
symbol,
None,
None,
Some("quote timeout"),
Some("not enough candles"),
)
}
fn result(
symbol: &str,
quote: Option<Value>,
analysis: Option<DashboardOutput>,
quote_error: Option<&str>,
analysis_error: Option<&str>,
) -> AnalyzeSymbolResult {
AnalyzeSymbolResult {
symbol: symbol.to_string(),
quote,
analysis,
quote_error: quote_error.map(str::to_string),
analysis_error: analysis_error.map(str::to_string),
}
}
fn quote_value(symbol: &str) -> Value {
json!({
symbol: {
"assetMainType": "EQUITY",
"symbol": symbol,
"quote": { "lastPrice": 123.45 }
}
})
}
fn sample_point() -> TaPoint {
TaPoint {
timestamp: 1,
value: 2.5,
}
}
fn sample_dashboard(symbol: &str) -> DashboardOutput {
DashboardOutput {
symbol: symbol.to_string(),
interval: "daily".to_string(),
points: 1,
trend: TrendIndicators {
sma_21: vec![sample_point()],
sma_50: vec![sample_point()],
sma_200: vec![sample_point()],
ema_21: vec![sample_point()],
},
momentum: MomentumIndicators {
rsi_14: vec![sample_point()],
macd: vec![MacdPoint {
timestamp: 1,
macd: 1.0,
signal: 0.5,
histogram: 0.5,
}],
stochastic: vec![StochPoint {
timestamp: 1,
k: 50.0,
d: 45.0,
}],
adx: vec![AdxPoint {
timestamp: 1,
adx: 25.0,
plus_di: 30.0,
minus_di: 20.0,
}],
},
volatility: VolatilityIndicators {
atr_14: vec![sample_point()],
bollinger_bands: vec![BbandsPoint {
timestamp: 1,
upper: 110.0,
middle: 100.0,
lower: 90.0,
}],
historical_volatility: vec![sample_point()],
},
volume: VolumeIndicators {
vwap: vec![sample_point()],
avg_volume_20: vec![sample_point()],
relative_volume: Some(1.2),
},
derived: DerivedFields {
atr_percent: 1.0,
range_20_high: 110.0,
range_20_low: 90.0,
range_252_high: 150.0,
range_252_low: 80.0,
distance_from_sma_21: 0.1,
distance_from_sma_50: 0.2,
distance_from_sma_200: 0.3,
},
signals: SignalSummary {
trend: TrendSignal {
above_sma_21: true,
above_sma_50: true,
above_sma_200: true,
sma_21_above_sma_50: true,
sma_50_above_sma_200: true,
},
momentum: MomentumSignal {
rsi_overbought: false,
rsi_oversold: false,
macd_bullish: true,
stoch_overbought: false,
stoch_oversold: false,
adx_trending: true,
},
volatility: VolatilitySignal {
atr_elevated: false,
price_near_upper_band: false,
price_near_lower_band: false,
},
volume: VolumeSignal {
high_relative_volume: true,
price_above_vwap: true,
},
},
}
}