feat(research,cli): the CostSpec campaign vocabulary (#234 task 2)
CampaignDoc gains an additive cost: Vec<CostSpec> block — a closed, externally tagged vocabulary over the three shipped cost nodes (constant/vol_slippage/carry, field names = the builders' ParamSpec names), mirroring the risk block: serde default + skip-if-empty (cost- less documents keep their content ids, pinned), DocFault::BadCost validation (finite/bounds per component), std::cost construct- vocabulary block + SlotKind::Costs + open-slot mirror, prose arm, and all CampaignDoc struct literals across crates carry cost: vec![] (compile-driven). Verified: aura-research suite green, campaign validate e2e green, full workspace suite green, clippy -D warnings clean; in-loop spec review compliant and quality review approved (real diff fingerprint — the worktree-anchored review prompt works). refs #234
This commit is contained in:
@@ -478,6 +478,7 @@ mod tests {
|
||||
bindings: BTreeMap::new(),
|
||||
},
|
||||
risk: vec![],
|
||||
cost: vec![],
|
||||
strategies: vec![StrategyEntry {
|
||||
r#ref: DocRef::ContentId("0".repeat(64)),
|
||||
axes: BTreeMap::from([(
|
||||
@@ -905,6 +906,7 @@ mod wf_tests {
|
||||
bindings: BTreeMap::new(),
|
||||
},
|
||||
risk: vec![],
|
||||
cost: vec![],
|
||||
strategies: vec![StrategyEntry {
|
||||
r#ref: DocRef::ContentId("c".repeat(64)),
|
||||
axes: BTreeMap::from([(
|
||||
|
||||
@@ -152,6 +152,7 @@ fn campaign(instruments: &[&str]) -> CampaignDoc {
|
||||
bindings: BTreeMap::new(),
|
||||
},
|
||||
risk: vec![],
|
||||
cost: vec![],
|
||||
strategies: vec![StrategyEntry {
|
||||
r#ref: DocRef::ContentId(STRATEGY_ID.to_string()),
|
||||
axes: axes_2x2(),
|
||||
|
||||
@@ -144,6 +144,9 @@ pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
|
||||
DocFault::BadRegime { index } => {
|
||||
format!("risk[{index}]: stop length must be >= 1 and k must be > 0")
|
||||
}
|
||||
DocFault::BadCost { index } => {
|
||||
format!("cost[{index}]: the component's price-unit knob must be finite and >= 0")
|
||||
}
|
||||
DocFault::NoStrategy => "strategies is empty".into(),
|
||||
DocFault::EmptyAxes { strategy } => format!("strategies[{strategy}]: axes is empty"),
|
||||
DocFault::EmptyAxis { strategy, axis } => {
|
||||
|
||||
@@ -162,6 +162,7 @@ pub(crate) fn translate_sweep(inv: &SugarInvocation) -> Result<GeneratedSweep, S
|
||||
// established parity). The other verbs bind the single Vol regime
|
||||
// through this same seam.
|
||||
risk: risk_from(inv.stop),
|
||||
cost: vec![],
|
||||
// No stage of a selection-free single-sweep pipeline consumes the
|
||||
// seed; a fixed zero keeps generated bytes deterministic so identical
|
||||
// invocations dedupe onto identical content ids.
|
||||
@@ -241,6 +242,7 @@ pub(crate) fn translate_generalize(
|
||||
// The stop is a single structural risk regime; the member runner maps
|
||||
// it back to StopRule::Vol at run time.
|
||||
risk: risk_from(inv.stop),
|
||||
cost: vec![],
|
||||
seed: 0,
|
||||
presentation: Presentation {
|
||||
persist_taps: vec![],
|
||||
@@ -441,6 +443,7 @@ pub(crate) fn translate_walkforward(
|
||||
}],
|
||||
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
||||
risk: risk_from(inv.stop),
|
||||
cost: vec![],
|
||||
seed: 0,
|
||||
presentation: Presentation {
|
||||
persist_taps: persist_taps_from(inv.trace),
|
||||
@@ -596,6 +599,7 @@ pub(crate) fn translate_mc(
|
||||
}],
|
||||
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
||||
risk: risk_from(inv.stop),
|
||||
cost: vec![],
|
||||
seed: mc.seed,
|
||||
presentation: Presentation {
|
||||
persist_taps: vec![],
|
||||
|
||||
@@ -597,7 +597,7 @@ fn campaign_introspect_vocabulary_lists_sections() {
|
||||
// #216: std::risk joins the roster the CLI's own --vocabulary listing
|
||||
// enumerates — the regime axis is discoverable from the public surface,
|
||||
// not only from the design ledger or a generated document.
|
||||
for id in ["std::data", "std::strategy", "std::process_ref", "std::presentation", "std::risk"] {
|
||||
for id in ["std::data", "std::strategy", "std::process_ref", "std::presentation", "std::risk", "std::cost"] {
|
||||
assert!(out.contains(id), "vocabulary misses {id}: {out}");
|
||||
}
|
||||
}
|
||||
@@ -625,6 +625,23 @@ fn campaign_introspect_block_risk_describes_the_regime_shape() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #234: `--block std::cost` names the component shape and its
|
||||
/// absent-means-zero-costs semantics — the cost model is discoverable from
|
||||
/// the public introspection surface (the `std::risk` #216 precedent).
|
||||
#[test]
|
||||
fn campaign_introspect_block_cost_describes_the_component_shape() {
|
||||
let (out, code) = run_code(&["campaign", "introspect", "--block", "std::cost"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)"),
|
||||
"the cost slot must render as optional with the component shape: {out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("std::cost — campaign section: the cost model"),
|
||||
"stdout/stderr: {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #216: a complete-but-risk-less campaign document (the exact shape a
|
||||
/// verb-generated document has before a `--stop-*` flag is added) has its one
|
||||
/// remaining open slot — the optional risk axis — enumerated by the public
|
||||
@@ -660,22 +677,23 @@ fn campaign_introspect_unwired_lists_the_optional_risk_slot_on_bare_envelope() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #216: a bound (non-empty) risk list closes the slot — a complete document
|
||||
/// with risk reports no open slots at all.
|
||||
/// #216/#234: bound (non-empty) risk AND cost lists close both optional
|
||||
/// slots — a complete document with both reports no open slots at all.
|
||||
#[test]
|
||||
fn campaign_introspect_unwired_omits_a_bound_risk_slot() {
|
||||
fn campaign_introspect_unwired_omits_bound_risk_and_cost_slots() {
|
||||
let dir = temp_cwd("campaign-introspect-unwired-risk-bound");
|
||||
let with_risk = CAMPAIGN_DOC.replacen(
|
||||
let with_both = CAMPAIGN_DOC.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(with_risk, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "bound.campaign.json", &with_risk);
|
||||
assert_ne!(with_both, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "bound.campaign.json", &with_both);
|
||||
let (out, code) =
|
||||
run_code_in(&dir, &["campaign", "introspect", "--unwired", "bound.campaign.json"]);
|
||||
assert_eq!(code, Some(0));
|
||||
assert!(!out.contains("open slot: risk"), "bound risk must not list: {out}");
|
||||
assert!(!out.contains("open slot: cost"), "bound cost must not list: {out}");
|
||||
assert!(out.contains("no open slots"), "stdout/stderr: {out}");
|
||||
}
|
||||
|
||||
@@ -965,6 +983,33 @@ fn campaign_register_refuses_invalid_risk_section_and_writes_nothing() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #234: the register validate-gate extends to the cost model — a document
|
||||
/// whose only fault is a negative cost knob is refused with prose (exit 1)
|
||||
/// and no file is ever written under `runs/campaigns/` (the risk-section
|
||||
/// precedent).
|
||||
#[test]
|
||||
fn campaign_register_refuses_invalid_cost_section_and_writes_nothing() {
|
||||
let dir = temp_cwd("campaign-register-invalid-cost");
|
||||
let bad = CAMPAIGN_DOC.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": -2.0 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(bad, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "bad-cost.campaign.json", &bad);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "register", "bad-cost.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("refusing to register:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("cost[0]: the component's price-unit knob must be finite and >= 0"),
|
||||
"stdout/stderr: {out}"
|
||||
);
|
||||
assert!(
|
||||
!dir.join("runs").join("campaigns").exists(),
|
||||
"register must not create a store entry for an invalid cost section"
|
||||
);
|
||||
}
|
||||
|
||||
/// Seed one open-param blueprint into the built demo project's store via a
|
||||
/// real sweep and return its content id (the referential test's recipe).
|
||||
fn seed_blueprint(dir: &Path, name: &str) -> String {
|
||||
|
||||
+175
-12
@@ -159,6 +159,7 @@ pub enum SlotKind {
|
||||
/// The closed tap vocabulary (`tap_vocabulary()` entries).
|
||||
TapKinds,
|
||||
Regimes,
|
||||
Costs,
|
||||
}
|
||||
|
||||
/// One typed slot of a block.
|
||||
@@ -436,6 +437,12 @@ pub struct CampaignDoc {
|
||||
/// parity for every risk-less document (the `description` precedent).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub risk: Vec<RiskRegime>,
|
||||
/// The campaign's cost model (#234): additive components (C10) every
|
||||
/// member runs under. Absent or empty = zero costs (`summarize_r`'s
|
||||
/// empty-slice baseline: net == gross). Absent-serializes → content-id
|
||||
/// parity for every cost-less document (the `risk` precedent above).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub cost: Vec<CostSpec>,
|
||||
pub strategies: Vec<StrategyEntry>,
|
||||
pub process: ProcessRef,
|
||||
pub seed: u64,
|
||||
@@ -496,6 +503,20 @@ pub enum RiskRegime {
|
||||
Vol { length: i64, k: f64 },
|
||||
}
|
||||
|
||||
/// One component of the campaign's cost model (#234): a closed, additive
|
||||
/// vocabulary over the shipped C10 cost nodes — components sum (`CostSum`).
|
||||
/// Serde field names are the builders' own `ParamSpec` names
|
||||
/// (`constant_cost.rs` / `vol_slippage_cost.rs` / `carry_cost.rs`), so the doc
|
||||
/// vocabulary conforms to the node vocabulary. Externally tagged so a future
|
||||
/// component is additive — no content-id churn on stored components.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "snake_case")]
|
||||
pub enum CostSpec {
|
||||
Constant { cost_per_trade: f64 },
|
||||
VolSlippage { slip_vol_mult: f64 },
|
||||
Carry { carry_per_cycle: f64 },
|
||||
}
|
||||
|
||||
/// Bare wire value of one Scalar (strips the tagged form via Scalar's own
|
||||
/// serde as the oracle — no Scalar internals assumed).
|
||||
fn scalar_to_bare(s: &Scalar) -> serde_json::Value {
|
||||
@@ -702,6 +723,7 @@ pub enum DocFault {
|
||||
NoWindow,
|
||||
BadWindow { index: usize },
|
||||
BadRegime { index: usize },
|
||||
BadCost { index: usize },
|
||||
NoStrategy,
|
||||
EmptyAxes { strategy: usize },
|
||||
EmptyAxis { strategy: usize, axis: String },
|
||||
@@ -812,6 +834,20 @@ pub fn validate_campaign(doc: &CampaignDoc) -> Vec<DocFault> {
|
||||
faults.push(DocFault::BadRegime { index: i });
|
||||
}
|
||||
}
|
||||
for (i, c) in doc.cost.iter().enumerate() {
|
||||
// Every shipped cost node asserts a finite, non-negative price-unit
|
||||
// knob (`>= 0`); the doc tier refuses the same bounds earlier,
|
||||
// path-addressed (the BadRegime precedent). NaN/inf are unreachable
|
||||
// via JSON but refused defensively.
|
||||
let v = match c {
|
||||
CostSpec::Constant { cost_per_trade } => *cost_per_trade,
|
||||
CostSpec::VolSlippage { slip_vol_mult } => *slip_vol_mult,
|
||||
CostSpec::Carry { carry_per_cycle } => *carry_per_cycle,
|
||||
};
|
||||
if !v.is_finite() || v < 0.0 {
|
||||
faults.push(DocFault::BadCost { index: i });
|
||||
}
|
||||
}
|
||||
if doc.strategies.is_empty() {
|
||||
faults.push(DocFault::NoStrategy);
|
||||
}
|
||||
@@ -863,6 +899,11 @@ pub const CAMPAIGN_SECTIONS: &[BlockSchema] = &[
|
||||
doc: "campaign section: the structural risk axis — stop regimes the matrix runs each cell under (absent = one default regime)",
|
||||
slots: &[SlotInfo { name: "risk", kind: SlotKind::Regimes, required: false }],
|
||||
},
|
||||
BlockSchema {
|
||||
id: "std::cost",
|
||||
doc: "campaign section: the cost model — additive components (C10) every member runs under (absent = zero costs; net == gross)",
|
||||
slots: &[SlotInfo { name: "cost", kind: SlotKind::Costs, required: false }],
|
||||
},
|
||||
BlockSchema {
|
||||
id: "std::strategy",
|
||||
doc: "campaign section: strategy ref (content or identity id) + param axes",
|
||||
@@ -906,6 +947,9 @@ pub fn slot_kind_label(kind: SlotKind) -> &'static str {
|
||||
SlotKind::Regimes => {
|
||||
"list of stop regimes { vol: { length, k } }; absent = one default regime"
|
||||
}
|
||||
SlotKind::Costs => {
|
||||
"list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,6 +1069,15 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
|
||||
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime",
|
||||
));
|
||||
}
|
||||
// The optional cost slot, mirroring the risk axis (#216's discoverability
|
||||
// lesson applied to #234): absent, empty, or non-array = open.
|
||||
let cost_bound = v.get("cost").and_then(|c| c.as_array()).is_some_and(|a| !a.is_empty());
|
||||
if !cost_bound {
|
||||
slots.push(open(
|
||||
"cost",
|
||||
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)",
|
||||
));
|
||||
}
|
||||
match v.get("strategies").and_then(|s| s.as_array()) {
|
||||
None => slots.push(open("strategies", "required, non-empty list of { ref, axes }")),
|
||||
Some(list) if list.is_empty() => {
|
||||
@@ -1392,6 +1445,98 @@ mod tests {
|
||||
assert!(validate_campaign(&doc).is_empty(), "{:?}", validate_campaign(&doc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_spec_round_trips_as_externally_tagged_components() {
|
||||
// Serde field names ARE the shipped builders' ParamSpec names
|
||||
// (cost_per_trade / slip_vol_mult / carry_per_cycle) — the doc
|
||||
// vocabulary conforms to the node vocabulary, not the other way round.
|
||||
for (spec, wire) in [
|
||||
(
|
||||
CostSpec::Constant { cost_per_trade: 2.0 },
|
||||
r#"{"constant":{"cost_per_trade":2.0}}"#,
|
||||
),
|
||||
(
|
||||
CostSpec::VolSlippage { slip_vol_mult: 0.5 },
|
||||
r#"{"vol_slippage":{"slip_vol_mult":0.5}}"#,
|
||||
),
|
||||
(
|
||||
CostSpec::Carry { carry_per_cycle: 0.1 },
|
||||
r#"{"carry":{"carry_per_cycle":0.1}}"#,
|
||||
),
|
||||
] {
|
||||
assert_eq!(serde_json::to_string(&spec).unwrap(), wire);
|
||||
let back: CostSpec = serde_json::from_str(wire).unwrap();
|
||||
assert_eq!(back, spec);
|
||||
}
|
||||
// deny_unknown_fields: a misspelled knob refuses to parse
|
||||
assert!(serde_json::from_str::<CostSpec>(r#"{"constant":{"per_close_r":2.0}}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_absent_and_empty_cost_omit_from_canonical_bytes() {
|
||||
// The risk precedent's sibling: cost-less documents keep their content ids.
|
||||
let doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
|
||||
assert!(doc.cost.is_empty(), "an absent cost key deserializes to empty");
|
||||
let absent = campaign_to_json(&doc);
|
||||
assert!(!absent.contains("\"cost\""), "absent cost omits from serialization: {absent}");
|
||||
let mut empty = doc.clone();
|
||||
empty.cost = vec![];
|
||||
assert_eq!(
|
||||
content_id_of(&campaign_to_json(&empty)),
|
||||
content_id_of(&absent),
|
||||
"an explicit empty cost hashes identically to an absent one"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_with_cost_serializes_and_round_trips_the_components() {
|
||||
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
|
||||
doc.cost = vec![
|
||||
CostSpec::Constant { cost_per_trade: 2.0 },
|
||||
CostSpec::VolSlippage { slip_vol_mult: 0.5 },
|
||||
];
|
||||
let out = campaign_to_json(&doc);
|
||||
assert!(
|
||||
out.contains(r#""cost":[{"constant":{"cost_per_trade":2.0}},{"vol_slippage":{"slip_vol_mult":0.5}}]"#),
|
||||
"cost serializes in place as a tagged list: {out}"
|
||||
);
|
||||
let back: CampaignDoc = serde_json::from_str(&out).unwrap();
|
||||
assert_eq!(back.cost, doc.cost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_campaign_flags_each_bad_cost_component() {
|
||||
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
|
||||
doc.cost = vec![
|
||||
CostSpec::Constant { cost_per_trade: 0.0 }, // 0: valid (zero cost is a legal stress point)
|
||||
CostSpec::VolSlippage { slip_vol_mult: -0.5 }, // 1: negative
|
||||
CostSpec::Carry { carry_per_cycle: f64::NAN }, // 2: non-finite (unreachable via JSON; defensive)
|
||||
];
|
||||
let faults = validate_campaign(&doc);
|
||||
assert!(faults.contains(&DocFault::BadCost { index: 1 }), "{faults:?}");
|
||||
assert!(faults.contains(&DocFault::BadCost { index: 2 }), "{faults:?}");
|
||||
assert!(!faults.contains(&DocFault::BadCost { index: 0 }), "valid component not flagged: {faults:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_campaign_accepts_a_valid_cost_section() {
|
||||
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
|
||||
doc.cost = vec![CostSpec::Carry { carry_per_cycle: 0.25 }];
|
||||
assert!(validate_campaign(&doc).is_empty(), "{:?}", validate_campaign(&doc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_block_is_describable_beside_risk() {
|
||||
let block = describe_block("std::cost").expect("std::cost describable");
|
||||
assert_eq!(block.slots.len(), 1);
|
||||
assert_eq!(block.slots[0].name, "cost");
|
||||
assert!(!block.slots[0].required);
|
||||
assert_eq!(
|
||||
slot_kind_label(block.slots[0].kind),
|
||||
"list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)"
|
||||
);
|
||||
}
|
||||
|
||||
/// The intrinsic tier does NOT constrain where a population-transforming
|
||||
/// stage sits relative to the annotators (terminal_seen gates only Gate
|
||||
/// stages): `[sweep, monte_carlo, walk_forward]` is intrinsically valid.
|
||||
@@ -1782,14 +1927,20 @@ mod tests {
|
||||
#[test]
|
||||
fn open_slots_report_partial_documents_by_path() {
|
||||
assert_eq!(open_slots_process(PROCESS_FIXTURE).unwrap(), Vec::new());
|
||||
// #216: a complete risk-less campaign has exactly ONE open slot — the
|
||||
// optional risk axis (the probe's one optional line).
|
||||
// #216/#234: a complete but risk- and cost-less campaign has exactly
|
||||
// TWO open slots — the two optional axes, in section order.
|
||||
assert_eq!(
|
||||
open_slots_campaign(CAMPAIGN_FIXTURE).unwrap(),
|
||||
vec![open(
|
||||
"risk",
|
||||
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime",
|
||||
)]
|
||||
vec![
|
||||
open(
|
||||
"risk",
|
||||
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime",
|
||||
),
|
||||
open(
|
||||
"cost",
|
||||
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)",
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
let partial = r#"{ "format_version": 1, "kind": "process",
|
||||
@@ -1848,19 +1999,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// #216: the optional risk slot is listed only while unbound — a document
|
||||
/// with a non-empty risk list closes it (a complete bound document has no
|
||||
/// open slots at all; the risk-less complement is pinned in
|
||||
/// `open_slots_report_partial_documents_by_path`).
|
||||
/// #216/#234: each optional slot is listed only while unbound — binding
|
||||
/// risk leaves exactly the cost slot open; binding both closes the probe
|
||||
/// (a complete bound document has no open slots at all).
|
||||
#[test]
|
||||
fn open_slots_omit_a_bound_risk_slot() {
|
||||
fn open_slots_omit_bound_risk_and_cost_slots() {
|
||||
let with_risk = CAMPAIGN_FIXTURE.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(with_risk, CAMPAIGN_FIXTURE, "replacen must match the fixture's seed field");
|
||||
assert_eq!(open_slots_campaign(&with_risk).unwrap(), Vec::new());
|
||||
assert_eq!(
|
||||
open_slots_campaign(&with_risk).unwrap(),
|
||||
vec![open(
|
||||
"cost",
|
||||
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)",
|
||||
)]
|
||||
);
|
||||
let with_both = CAMPAIGN_FIXTURE.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(with_both, CAMPAIGN_FIXTURE, "replacen must match the fixture's seed field");
|
||||
assert_eq!(open_slots_campaign(&with_both).unwrap(), Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user