use std::convert::Infallible;
use chrono::NaiveDateTime;
use qs_risk::{HaltCommand, HaltInterval, IntentKind, RiskConfigError, Verdict};
use serde::Serialize;
use super::{
AnalysisPipeline, BacktestConfiguredStrategyAdapter, BarSeriesSpec,
ConfiguredStrategyAdapterError, HistoricalStrategy, StrategyDecisionOutput, StrategyDescriptor,
StrategyReplayError, StrategyReplayInputError, StrategyResearchOutput,
};
use crate::profile::PreparedEntryProfiles;
use crate::report::BacktestResult;
pub const MAX_PORTFOLIO_INSTANCES: usize = 64;
pub const INSTANCE_POSITION_TAG: &str = "instance";
pub struct ConfiguredInstance {
pub adapter: BacktestConfiguredStrategyAdapter,
pub analysis: AnalysisPipeline,
pub entry_profiles: PreparedEntryProfiles,
pub feed_from: Option<NaiveDateTime>,
}
impl ConfiguredInstance {
pub fn new(adapter: BacktestConfiguredStrategyAdapter, analysis: AnalysisPipeline) -> Self {
Self {
adapter,
analysis,
entry_profiles: PreparedEntryProfiles::default(),
feed_from: None,
}
}
pub fn with_entry_profiles(mut self, profiles: PreparedEntryProfiles) -> Self {
self.entry_profiles = profiles;
self
}
pub fn with_feed_from(mut self, from: NaiveDateTime) -> Self {
self.feed_from = Some(from);
self
}
pub fn strategy_id(&self) -> &str {
self.adapter.configured_strategy().strategy_id()
}
pub fn instance_id(&self) -> &str {
self.adapter.configured_strategy().instance_id()
}
}
pub struct DirectPortfolioInstance<E> {
pub instance_id: String,
pub strategy: Box<dyn HistoricalStrategy<Error = E> + Send>,
pub series: Vec<BarSeriesSpec>,
pub analysis: AnalysisPipeline,
pub entry_profiles: PreparedEntryProfiles,
pub feed_from: Option<NaiveDateTime>,
}
impl<E> DirectPortfolioInstance<E> {
pub fn new(
instance_id: impl Into<String>,
strategy: Box<dyn HistoricalStrategy<Error = E> + Send>,
series: Vec<BarSeriesSpec>,
analysis: AnalysisPipeline,
) -> Self {
Self {
instance_id: instance_id.into(),
strategy,
series,
analysis,
entry_profiles: PreparedEntryProfiles::default(),
feed_from: None,
}
}
pub fn with_entry_profiles(mut self, profiles: PreparedEntryProfiles) -> Self {
self.entry_profiles = profiles;
self
}
pub fn with_feed_from(mut self, from: NaiveDateTime) -> Self {
self.feed_from = Some(from);
self
}
}
#[derive(Debug)]
pub struct MixedPortfolioBacktestResult {
pub replay: BacktestResult,
pub supervisor: Option<SupervisorOutput>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PortfolioInstanceOutput {
pub strategy_id: String,
pub instance_id: String,
pub descriptor: StrategyDescriptor,
pub decisions: StrategyDecisionOutput,
pub research: StrategyResearchOutput,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SupervisorEvent {
pub ts: NaiveDateTime,
pub instance_id: String,
pub action_id: String,
pub kind: IntentKind,
pub symbol: String,
pub requested_risk: Option<f64>,
pub verdict: Verdict,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SupervisorHaltAction {
pub ts: NaiveDateTime,
pub action_id: String,
pub command: HaltCommand,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SupervisorOutput {
pub events: Vec<SupervisorEvent>,
pub halt_actions: Vec<SupervisorHaltAction>,
pub halts: Vec<HaltInterval>,
pub unmarked_boundaries: u64,
}
impl SupervisorOutput {
pub fn rejected_entries(&self) -> usize {
self.events
.iter()
.filter(|event| event.kind == IntentKind::Entry && !event.verdict.is_approved())
.count()
}
pub fn halt_minutes(&self, run_end: NaiveDateTime) -> i64 {
self.halts
.iter()
.map(|halt| {
let end = halt.to.unwrap_or(run_end).max(halt.from);
(end - halt.from).num_minutes()
})
.sum()
}
}
#[derive(Debug)]
pub struct PortfolioBacktestResult {
pub replay: BacktestResult,
pub instances: Vec<PortfolioInstanceOutput>,
pub supervisor: Option<SupervisorOutput>,
}
#[derive(Debug, thiserror::Error)]
pub enum PortfolioReplayError<FeedError> {
#[error("a portfolio replay needs at least one instance")]
NoInstances,
#[error("a portfolio replay accepts at most {MAX_PORTFOLIO_INSTANCES} instances, got {0}")]
TooManyInstances(usize),
#[error(
"instance '{instance_id}' appears more than once; every instance of a portfolio needs its own identifier"
)]
DuplicateInstanceIdentity { instance_id: String },
#[error("portfolio replay input is invalid: {0}")]
Input(#[from] StrategyReplayInputError),
#[error("portfolio supervisor is invalid: {0}")]
Supervisor(String),
#[error("instance '{instance_id}' failed: {source}")]
Instance {
instance_id: String,
#[source]
source: StrategyReplayError<Infallible, ConfiguredStrategyAdapterError>,
},
#[error(
"the portfolio feed mixes ticks and stored bars from {timestamp}; a portfolio replays one kind of primary input"
)]
MixedPrimaryInput { timestamp: NaiveDateTime },
#[error("market-data stream failed: {0}")]
Feed(FeedError),
#[error("portfolio replay was cancelled")]
Cancelled,
}
#[derive(Debug, thiserror::Error)]
pub enum MixedPortfolioReplayError<FeedError> {
#[error("a mixed portfolio replay needs at least one instance")]
NoInstances,
#[error(
"a mixed portfolio replay accepts at most {MAX_PORTFOLIO_INSTANCES} instances, got {0}"
)]
TooManyInstances(usize),
#[error("instance '{instance_id}' appears more than once")]
DuplicateInstanceIdentity { instance_id: String },
#[error("mixed portfolio replay input is invalid: {0}")]
Input(String),
#[error("instance '{instance_id}' failed: {reason}")]
Instance { instance_id: String, reason: String },
#[error("the mixed portfolio feed mixes ticks and stored bars from {timestamp}")]
MixedPrimaryInput { timestamp: NaiveDateTime },
#[error("market-data stream failed: {0}")]
Feed(FeedError),
#[error("mixed portfolio replay was cancelled")]
Cancelled,
}
impl<FeedError> From<RiskConfigError> for PortfolioReplayError<FeedError> {
fn from(error: RiskConfigError) -> Self {
Self::Supervisor(error.to_string())
}
}