From 49edaef14d0c3b6ffde25fad802d43763e295307 Mon Sep 17 00:00:00 2001 From: Sonic Shih Date: Wed, 22 Jul 2026 15:55:12 +0800 Subject: [PATCH 1/2] refactor(research): unify Formula MCTS state Move Formula Alpha Search state, selection, backpropagation, budget, and resume onto the shared UCT kernel adapter. Publish deterministic read-only projections and reject legacy or forged checkpoints fail closed.\n\nCloses #203 --- .../polymarket-btc-5m.example.json | 2 +- .../polymarket-sol-5m.example.json | 2 +- .../crates/ploy-research/src/alpha_search.rs | 798 +++++++++++++----- .../src/bin/monday-prediction-evaluator.rs | 23 +- .../crates/ploy-research/src/lib.rs | 5 +- .../docs/ALPHA_FACTOR_SEARCH_CICD.md | 27 +- 6 files changed, 636 insertions(+), 221 deletions(-) diff --git a/rust_hft/prediction-markets/config/research_missions/polymarket-btc-5m.example.json b/rust_hft/prediction-markets/config/research_missions/polymarket-btc-5m.example.json index 79e024c1d..8ca305086 100644 --- a/rust_hft/prediction-markets/config/research_missions/polymarket-btc-5m.example.json +++ b/rust_hft/prediction-markets/config/research_missions/polymarket-btc-5m.example.json @@ -15,7 +15,7 @@ "horizon": "5m", "time_cohort_boundary_ms": 0, "prompt_snapshot_id": "sha256:2b55ba0e724dfc9f5a040911e397058a09c6229e42064e4998739d617b368dcb", - "search_policy_snapshot_id": "sha256:a0784791820be66937f2b4178466866d340f4fd4da19ba4d2bc7fd4cb881ff9b", + "search_policy_snapshot_id": "sha256:e98b091da59ca2878c79d817b5d774cc8f3591384730eea93b76499f0c0eea79", "search_budget": { "max_candidates": 6, "max_llm_calls": 2, diff --git a/rust_hft/prediction-markets/config/research_missions/polymarket-sol-5m.example.json b/rust_hft/prediction-markets/config/research_missions/polymarket-sol-5m.example.json index 04a70eeb1..eed10efa3 100644 --- a/rust_hft/prediction-markets/config/research_missions/polymarket-sol-5m.example.json +++ b/rust_hft/prediction-markets/config/research_missions/polymarket-sol-5m.example.json @@ -15,7 +15,7 @@ "horizon": "5m", "time_cohort_boundary_ms": 0, "prompt_snapshot_id": "sha256:0816ebccf4c75ee6bdcfe315b253c84ddf1808a38687ebe3b924b87bd72a52a9", - "search_policy_snapshot_id": "sha256:a0784791820be66937f2b4178466866d340f4fd4da19ba4d2bc7fd4cb881ff9b", + "search_policy_snapshot_id": "sha256:e98b091da59ca2878c79d817b5d774cc8f3591384730eea93b76499f0c0eea79", "search_budget": { "max_candidates": 6, "max_llm_calls": 2, diff --git a/rust_hft/prediction-markets/crates/ploy-research/src/alpha_search.rs b/rust_hft/prediction-markets/crates/ploy-research/src/alpha_search.rs index 2ec672d26..c9063b1c5 100644 --- a/rust_hft/prediction-markets/crates/ploy-research/src/alpha_search.rs +++ b/rust_hft/prediction-markets/crates/ploy-research/src/alpha_search.rs @@ -4,6 +4,11 @@ use std::path::Path; use serde::{Deserialize, Serialize}; +use hft_search_kernel::{ + backpropagate as kernel_backpropagate, select_expandable as kernel_select_expandable, + validate_tree as validate_kernel_tree, UctError, UctNode, UctStats, +}; + use crate::autofactor::{ autofactor_runtime_contract_catalog, autofactor_target_horizon, factor_expr_hash, AutoFactorDecision, AutoFactorOptions, AutoFactorReport, FactorExpr, LlmPriorSpec, @@ -12,6 +17,9 @@ use crate::factors_v2::ReviewSide; pub const ALPHA_SEARCH_ARTIFACT_VERSION: &str = "alpha_search_artifacts_v1"; pub const SIDE_BOUND_ALPHA_SEARCH_ARTIFACT_VERSION: &str = "alpha_search_artifacts_v2"; +pub const FORMULA_MCTS_CHECKPOINT_VERSION: &str = "formula_mcts_checkpoint_v1"; +const FORMULA_MCTS_ROOT_ID: &str = "__formula_mcts_root__"; +const FORMULA_MCTS_SELECTION_BUDGET: usize = 12; #[derive(Debug, Clone, Serialize)] pub struct AlphaSearchArtifactSummary { @@ -88,6 +96,34 @@ pub struct MctsSearchStateNode { pub last_decision: String, } +/// The only mutable Formula search state. It binds Formula-specific candidate +/// identity and metadata around the domain-neutral shared UCT kernel. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FormulaMctsCheckpoint { + pub version: String, + pub target: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub side: Option, + pub selection_budget: usize, + pub nodes: Vec, + #[serde(default)] + pub subtree_frequencies: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FormulaMctsCheckpointNode { + pub factor_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_name: Option, + pub visits: u64, + pub total_reward: f64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub best_reward: Option, + pub last_reward: f64, + pub selected_dimension: String, + pub last_decision: String, +} + /// A durable, cross-run snapshot of previously-accepted factors grouped by /// root gene, sourced from the `factor_registry` table across all historical /// runs. This is the "Alpha Zoo" from the "Navigating the Alpha Jungle" paper: @@ -126,6 +162,8 @@ pub enum AlphaSearchArtifactError { Io(std::io::Error), Json(serde_json::Error), IdentityMismatch(String), + LegacyCheckpointVersion(String), + Kernel(UctError), } impl fmt::Display for AlphaSearchArtifactError { @@ -136,6 +174,11 @@ impl fmt::Display for AlphaSearchArtifactError { Self::IdentityMismatch(reason) => { write!(f, "alpha search artifact identity mismatch: {reason}") } + Self::LegacyCheckpointVersion(version) => write!( + f, + "alpha search checkpoint version `{version}` is legacy; expected `{FORMULA_MCTS_CHECKPOINT_VERSION}`" + ), + Self::Kernel(err) => write!(f, "alpha search shared MCTS kernel failed: {err}"), } } } @@ -154,6 +197,12 @@ impl From for AlphaSearchArtifactError { } } +impl From for AlphaSearchArtifactError { + fn from(value: UctError) -> Self { + Self::Kernel(value) + } +} + #[derive(Debug, Serialize)] struct SearchSpaceArtifact { version: &'static str, @@ -409,7 +458,6 @@ struct MctsSelectedNode { selected_dimension: String, proposed_mutation: &'static str, reward: f64, - ucb_priority: f64, } pub fn write_alpha_search_artifacts( @@ -436,13 +484,32 @@ pub fn read_mcts_search_state( Ok(serde_json::from_str(&raw)?) } +pub fn read_formula_mcts_checkpoint( + path: impl AsRef, +) -> Result { + let raw = std::fs::read_to_string(path)?; + let value: serde_json::Value = serde_json::from_str(&raw)?; + let version = value + .get("version") + .and_then(serde_json::Value::as_str) + .unwrap_or(""); + if version != FORMULA_MCTS_CHECKPOINT_VERSION { + return Err(AlphaSearchArtifactError::LegacyCheckpointVersion( + version.to_string(), + )); + } + let checkpoint = serde_json::from_value::(value)?; + validate_formula_mcts_checkpoint(&checkpoint)?; + Ok(checkpoint) +} + pub fn write_alpha_search_artifacts_with_state( output_root: impl AsRef, target: &str, input_names: &[String], reports: &[AutoFactorReport], options: &AutoFactorOptions, - prior_state: Option<&MctsSearchStateArtifact>, + prior_state: Option<&FormulaMctsCheckpoint>, ) -> Result { write_alpha_search_artifacts_with_state_and_runtime_feedback( output_root, @@ -466,16 +533,14 @@ pub fn write_alpha_search_artifacts_with_state_and_runtime_feedback( input_names: &[String], reports: &[AutoFactorReport], options: &AutoFactorOptions, - prior_state: Option<&MctsSearchStateArtifact>, + prior_state: Option<&FormulaMctsCheckpoint>, runtime_feedback: Option<&AlphaSearchRuntimeFeedback>, llm_prior: Option<&LlmPriorSpec>, alpha_zoo: Option<&AlphaZooSnapshot>, ) -> Result { if is_side_bound_repricing_target(target) || reports.iter().any(|report| report.side.is_some()) - || prior_state.is_some_and(|state| { - state.version == SIDE_BOUND_ALPHA_SEARCH_ARTIFACT_VERSION || state.side.is_some() - }) + || prior_state.is_some_and(|state| state.side.is_some()) || runtime_feedback.is_some_and(|feedback| { feedback.version.as_deref() == Some(SIDE_BOUND_ALPHA_SEARCH_ARTIFACT_VERSION) || feedback.side.is_some() @@ -510,7 +575,7 @@ pub fn write_side_bound_alpha_search_artifacts_with_state_and_runtime_feedback( input_names: &[String], reports: &[AutoFactorReport], options: &AutoFactorOptions, - prior_state: Option<&MctsSearchStateArtifact>, + prior_state: Option<&FormulaMctsCheckpoint>, runtime_feedback: Option<&AlphaSearchRuntimeFeedback>, alpha_zoo: Option<&AlphaZooSnapshot>, ) -> Result { @@ -536,7 +601,7 @@ fn write_alpha_search_artifacts_core( input_names: &[String], reports: &[AutoFactorReport], options: &AutoFactorOptions, - prior_state: Option<&MctsSearchStateArtifact>, + prior_state: Option<&FormulaMctsCheckpoint>, runtime_feedback: Option<&AlphaSearchRuntimeFeedback>, llm_prior: Option<&LlmPriorSpec>, alpha_zoo: Option<&AlphaZooSnapshot>, @@ -684,31 +749,23 @@ fn write_alpha_search_artifacts_core( &output_dir.join("factor-registry-preview.json"), &factor_registry_preview_artifact(version, target, side, reports, &node_metrics)?, )?; - let mcts_state = mcts_search_state( + let checkpoint = formula_mcts_checkpoint( version, target, side, &node_metrics, prior_state, subtree_frequencies, - ); - let prior_truncated_count = prior_state - .filter(|state| state.target == target) - .map(|state| state.backpropagation_truncated_count) - .unwrap_or_default(); - let new_truncated_count = mcts_state - .backpropagation_truncated_count - .saturating_sub(prior_truncated_count); - if new_truncated_count > 0 { - eprintln!( - "alpha-search warning: MCTS backpropagation truncated {} new time(s) for target `{}` ({} cumulative); inspect parent_name lineage for cycles or impossible depth.", - new_truncated_count, target, mcts_state.backpropagation_truncated_count - ); - } + )?; + let mcts_state = mcts_search_state_projection(&checkpoint)?; + write_json( + &output_dir.join("formula-mcts-checkpoint.json"), + &checkpoint, + )?; write_json(&output_dir.join("mcts-state.json"), &mcts_state)?; write_json( &output_dir.join("mcts-expansion-plan.json"), - &mcts_expansion_plan(version, target, side, &node_metrics, &mcts_state), + &formula_mcts_expansion_plan(version, target, side, &checkpoint)?, )?; write_json( @@ -731,7 +788,7 @@ fn write_alpha_search_artifacts_core( report, &runtime_avoidances, alpha_zoo, - &mcts_state.subtree_frequencies, + &checkpoint.subtree_frequencies, ), visits: 1, decision: report.decision.as_str().to_string(), @@ -803,7 +860,7 @@ fn validate_side_bound_inputs( target: &str, side: ReviewSide, reports: &[AutoFactorReport], - prior_state: Option<&MctsSearchStateArtifact>, + prior_state: Option<&FormulaMctsCheckpoint>, runtime_feedback: Option<&AlphaSearchRuntimeFeedback>, alpha_zoo: Option<&AlphaZooSnapshot>, ) -> Result<(), AlphaSearchArtifactError> { @@ -824,14 +881,18 @@ fn validate_side_bound_inputs( } } if let Some(state) = prior_state { - validate_side_bound_identity( - "MCTS state", - Some(state.version.as_str()), - Some(state.target.as_str()), - state.side, - target, - side, - )?; + if state.version != FORMULA_MCTS_CHECKPOINT_VERSION + || state.target != target + || state.side != Some(side) + { + return Err(AlphaSearchArtifactError::IdentityMismatch(format!( + "Formula MCTS checkpoint expected target={target} side={}, found version={} target={} side={}", + side.as_str(), + state.version, + state.target, + state.side.map(ReviewSide::as_str).unwrap_or("") + ))); + } } if let Some(feedback) = runtime_feedback { validate_side_bound_identity( @@ -1425,16 +1486,27 @@ fn node_metric( } } -fn mcts_search_state( - version: &str, +fn formula_mcts_checkpoint( + _artifact_version: &str, target: &str, side: Option, metrics: &[NodeMetric], - prior_state: Option<&MctsSearchStateArtifact>, + prior_state: Option<&FormulaMctsCheckpoint>, subtree_frequencies: Vec, -) -> MctsSearchStateArtifact { - let mut nodes = prior_state - .filter(|state| state.target == target) +) -> Result { + if let Some(prior) = prior_state { + validate_formula_mcts_checkpoint(prior)?; + if prior.target != target || prior.side != side { + return Err(AlphaSearchArtifactError::IdentityMismatch(format!( + "Formula MCTS checkpoint expected target={target} side={}, found target={} side={}", + side.map(ReviewSide::as_str).unwrap_or(""), + prior.target, + prior.side.map(ReviewSide::as_str).unwrap_or("") + ))); + } + } + + let mut records = prior_state .map(|state| { state .nodes @@ -1443,135 +1515,351 @@ fn mcts_search_state( .collect::>() }) .unwrap_or_default(); + records + .entry(FORMULA_MCTS_ROOT_ID.to_string()) + .or_insert_with(|| FormulaMctsCheckpointNode { + factor_name: FORMULA_MCTS_ROOT_ID.to_string(), + parent_name: None, + visits: 0, + total_reward: 0.0, + best_reward: None, + last_reward: 0.0, + selected_dimension: "root".to_string(), + last_decision: "root".to_string(), + }); for metric in metrics { - let node = nodes - .entry(metric.factor_name.clone()) - .or_insert_with(|| MctsSearchStateNode { - factor_name: metric.factor_name.clone(), - parent_name: metric.parent_name.clone(), - visits: 0, - total_reward: 0.0, - best_reward: f64::NEG_INFINITY, - last_reward: 0.0, - selected_dimension: metric.selected_dimension.clone(), - last_decision: metric.decision.clone(), - }); - node.parent_name = metric.parent_name.clone(); + if metric.factor_name == FORMULA_MCTS_ROOT_ID { + return Err(AlphaSearchArtifactError::IdentityMismatch( + "Formula candidate identity uses the reserved MCTS root id".to_string(), + )); + } + let parent_name = metric.parent_name.clone(); + match records.get(&metric.factor_name) { + Some(node) if node.parent_name != parent_name => { + return Err(AlphaSearchArtifactError::IdentityMismatch(format!( + "Formula candidate `{}` changed parent from {:?} to {:?}", + metric.factor_name, node.parent_name, parent_name + ))); + } + Some(_) => {} + None => { + records.insert( + metric.factor_name.clone(), + FormulaMctsCheckpointNode { + factor_name: metric.factor_name.clone(), + parent_name, + visits: 0, + total_reward: 0.0, + best_reward: None, + last_reward: 0.0, + selected_dimension: metric.selected_dimension.clone(), + last_decision: metric.decision.clone(), + }, + ); + } + } } - let mut backpropagation_truncated_count = prior_state - .filter(|state| state.target == target) - .map(|state| state.backpropagation_truncated_count) - .unwrap_or_default(); - + let mut nodes = formula_mcts_kernel_nodes(records)?; for metric in metrics { - let Some(node) = nodes.get_mut(&metric.factor_name) else { - continue; + let node_id = nodes + .iter() + .position(|node| node.record.factor_name == metric.factor_name) + .ok_or_else(|| { + AlphaSearchArtifactError::IdentityMismatch(format!( + "Formula candidate `{}` disappeared from its MCTS checkpoint", + metric.factor_name + )) + })?; + nodes[node_id].record.last_reward = metric.reward; + nodes[node_id].record.selected_dimension = metric.selected_dimension.clone(); + nodes[node_id].record.last_decision = metric.decision.clone(); + kernel_backpropagate(&mut nodes, 0, node_id, metric.reward)?; + } + + validate_kernel_tree(&nodes, 0, usize::MAX)?; + Ok(FormulaMctsCheckpoint { + version: FORMULA_MCTS_CHECKPOINT_VERSION.to_string(), + target: target.to_string(), + side, + selection_budget: FORMULA_MCTS_SELECTION_BUDGET, + nodes: nodes.into_iter().map(|node| node.record).collect(), + subtree_frequencies, + }) +} + +fn mcts_search_state_projection( + checkpoint: &FormulaMctsCheckpoint, +) -> Result { + validate_formula_mcts_checkpoint(checkpoint)?; + let root = checkpoint + .nodes + .first() + .expect("validated Formula MCTS checkpoint has a root"); + let mut nodes = checkpoint + .nodes + .iter() + .filter(|node| node.factor_name != FORMULA_MCTS_ROOT_ID) + .map(|node| { + Ok(MctsSearchStateNode { + factor_name: node.factor_name.clone(), + parent_name: node.parent_name.clone(), + visits: usize::try_from(node.visits).map_err(|_| UctError::StatsOverflow)?, + total_reward: node.total_reward, + best_reward: node.best_reward.unwrap_or(0.0), + last_reward: node.last_reward, + selected_dimension: node.selected_dimension.clone(), + last_decision: node.last_decision.clone(), + }) + }) + .collect::, AlphaSearchArtifactError>>()?; + nodes.sort_by(|lhs, rhs| lhs.factor_name.cmp(&rhs.factor_name)); + Ok(MctsSearchStateArtifact { + version: FORMULA_MCTS_CHECKPOINT_VERSION.to_string(), + mode: "shared_kernel_projection".to_string(), + target: checkpoint.target.clone(), + side: checkpoint.side, + total_visits: usize::try_from(root.visits).map_err(|_| UctError::StatsOverflow)?, + backpropagation_truncated_count: 0, + nodes, + subtree_frequencies: checkpoint.subtree_frequencies.clone(), + }) +} + +#[cfg(test)] +fn mcts_search_state( + version: &str, + target: &str, + side: Option, + metrics: &[NodeMetric], + prior_state: Option<&FormulaMctsCheckpoint>, + subtree_frequencies: Vec, +) -> MctsSearchStateArtifact { + let checkpoint = formula_mcts_checkpoint( + version, + target, + side, + metrics, + prior_state, + subtree_frequencies, + ) + .expect("test Formula MCTS checkpoint"); + mcts_search_state_projection(&checkpoint).expect("test Formula MCTS projection") +} + +fn formula_mcts_expansion_plan( + version: &'static str, + target: &str, + side: Option, + checkpoint: &FormulaMctsCheckpoint, +) -> Result { + if checkpoint.target != target || checkpoint.side != side { + return Err(AlphaSearchArtifactError::IdentityMismatch( + "Formula MCTS checkpoint identity does not match expansion plan".to_string(), + )); + } + let exploration_weight = 0.75; + let mut nodes = formula_mcts_kernel_nodes( + checkpoint + .nodes + .iter() + .map(|node| (node.factor_name.clone(), node.clone())) + .collect(), + )?; + let mut selected = Vec::new(); + while selected.len() < checkpoint.selection_budget { + let Some(node_id) = kernel_select_expandable(&nodes, 0, exploration_weight)? else { + break; }; - node.visits = node.visits.saturating_add(1); - node.total_reward += metric.reward; - node.best_reward = node.best_reward.max(metric.reward); - node.last_reward = metric.reward; - node.selected_dimension = metric.selected_dimension.clone(); - node.last_decision = metric.decision.clone(); - if backpropagate(&mut nodes, &metric.factor_name, metric.reward) { - backpropagation_truncated_count = backpropagation_truncated_count.saturating_add(1); + if node_id == 0 { + break; } + let node = &mut nodes[node_id]; + node.expandable = false; + selected.push(MctsSelectedNode { + node_id: node.record.factor_name.clone(), + factor_name: node.record.factor_name.clone(), + selected_dimension: node.record.selected_dimension.clone(), + proposed_mutation: proposed_mutation(&node.record.selected_dimension), + reward: node.record.last_reward, + }); } - let mut nodes = nodes.into_values().collect::>(); - nodes.sort_by(|lhs, rhs| lhs.factor_name.cmp(&rhs.factor_name)); - let total_visits = nodes.iter().map(|node| node.visits).sum(); - MctsSearchStateArtifact { - version: version.to_string(), - mode: "cumulative_ucb_state".to_string(), + Ok(MctsExpansionPlan { + version, + mode: "shared_kernel_selection_projection", target: target.to_string(), side, - total_visits, - backpropagation_truncated_count, - nodes, - subtree_frequencies, + exploration_weight, + selected_nodes: selected, + note: "Formula candidate identity is adapter metadata around the shared UCT kernel; this plan is a read-only projection of kernel selection.", + }) +} + +#[derive(Clone)] +struct FormulaMctsKernelNode { + record: FormulaMctsCheckpointNode, + parent: Option, + children: Vec, + depth: usize, + expandable: bool, +} + +impl UctNode for FormulaMctsKernelNode { + fn parent(&self) -> Option { + self.parent + } + + fn children(&self) -> &[usize] { + &self.children + } + + fn is_expandable(&self) -> bool { + self.expandable + } + + fn depth(&self) -> usize { + self.depth + } + + fn stats(&self) -> Result { + UctStats::from_parts( + self.record.visits, + self.record.total_reward, + self.record.best_reward, + ) + } + + fn replace_stats(&mut self, stats: UctStats) { + self.record.visits = stats.visits(); + self.record.total_reward = stats.total_reward(); + self.record.best_reward = stats.best_reward(); } } -fn backpropagate( - nodes: &mut BTreeMap, - leaf_name: &str, - reward: f64, -) -> bool { - let mut current = leaf_name.to_string(); - let mut visited = BTreeSet::new(); - let max_hops = nodes.len().max(1); - for _ in 0..max_hops { - if !visited.insert(current.clone()) { - return true; +fn formula_mcts_kernel_nodes( + mut records: BTreeMap, +) -> Result, AlphaSearchArtifactError> { + let root = records.remove(FORMULA_MCTS_ROOT_ID).ok_or_else(|| { + AlphaSearchArtifactError::IdentityMismatch( + "Formula MCTS checkpoint is missing its reserved root node".to_string(), + ) + })?; + if root.parent_name.is_some() || root.factor_name != FORMULA_MCTS_ROOT_ID { + return Err(AlphaSearchArtifactError::IdentityMismatch( + "Formula MCTS checkpoint root identity is invalid".to_string(), + )); + } + let mut nodes = vec![FormulaMctsKernelNode { + record: root, + parent: None, + children: Vec::new(), + depth: 0, + expandable: false, + }]; + let mut by_name = BTreeMap::from([(FORMULA_MCTS_ROOT_ID.to_string(), 0usize)]); + + while !records.is_empty() { + let ready = records + .iter() + .filter_map(|(name, node)| { + let parent = node.parent_name.as_deref().unwrap_or(FORMULA_MCTS_ROOT_ID); + by_name.contains_key(parent).then_some(name.clone()) + }) + .collect::>(); + if ready.is_empty() { + let unresolved = records.keys().cloned().collect::>().join(", "); + return Err(AlphaSearchArtifactError::IdentityMismatch(format!( + "Formula MCTS candidate lineage is cyclic or references a missing parent: {unresolved}" + ))); + } + for name in ready { + let record = records.remove(&name).expect("ready Formula node exists"); + if record.factor_name != name { + return Err(AlphaSearchArtifactError::IdentityMismatch(format!( + "Formula MCTS checkpoint key `{name}` does not match candidate identity `{}`", + record.factor_name + ))); + } + let parent_name = record + .parent_name + .as_deref() + .unwrap_or(FORMULA_MCTS_ROOT_ID); + let parent = *by_name.get(parent_name).expect("ready parent is assigned"); + let depth = nodes[parent].depth.saturating_add(1); + let node_id = nodes.len(); + nodes[parent].children.push(node_id); + nodes.push(FormulaMctsKernelNode { + expandable: false, + record, + parent: Some(parent), + children: Vec::new(), + depth, + }); + by_name.insert(name, node_id); } - let Some(parent_name) = nodes - .get(¤t) - .and_then(|node| node.parent_name.clone()) - else { - return false; - }; - let Some(parent) = nodes.get_mut(&parent_name) else { - return false; - }; - parent.visits = parent.visits.saturating_add(1); - parent.total_reward += reward; - parent.best_reward = parent.best_reward.max(reward); - current = parent_name; } - true + for node in &mut nodes { + node.expandable = node.parent.is_some() + && node.children.is_empty() + && node.record.last_decision != "reject" + && node.record.selected_dimension != "runtime_executable_entry"; + } + validate_kernel_tree(&nodes, 0, usize::MAX)?; + Ok(nodes) +} + +fn validate_formula_mcts_checkpoint( + checkpoint: &FormulaMctsCheckpoint, +) -> Result<(), AlphaSearchArtifactError> { + if checkpoint.version != FORMULA_MCTS_CHECKPOINT_VERSION { + return Err(AlphaSearchArtifactError::LegacyCheckpointVersion( + checkpoint.version.clone(), + )); + } + if checkpoint.selection_budget != FORMULA_MCTS_SELECTION_BUDGET { + return Err(AlphaSearchArtifactError::IdentityMismatch(format!( + "Formula MCTS checkpoint selection budget {} does not match {FORMULA_MCTS_SELECTION_BUDGET}", + checkpoint.selection_budget + ))); + } + if checkpoint + .nodes + .first() + .map(|node| node.factor_name.as_str()) + != Some(FORMULA_MCTS_ROOT_ID) + { + return Err(AlphaSearchArtifactError::IdentityMismatch( + "Formula MCTS checkpoint root must be the first node".to_string(), + )); + } + let records = checkpoint + .nodes + .iter() + .map(|node| (node.factor_name.clone(), node.clone())) + .collect::>(); + if records.len() != checkpoint.nodes.len() { + return Err(AlphaSearchArtifactError::IdentityMismatch( + "Formula MCTS checkpoint contains duplicate candidate identities".to_string(), + )); + } + formula_mcts_kernel_nodes(records)?; + Ok(()) } +#[cfg(test)] fn mcts_expansion_plan( version: &'static str, target: &str, side: Option, metrics: &[NodeMetric], - state: &MctsSearchStateArtifact, + _state: &MctsSearchStateArtifact, ) -> MctsExpansionPlan { - let exploration_weight = 0.75; - let total_visits = state.total_visits.max(1) as f64; - let state_by_factor = state - .nodes - .iter() - .map(|node| (node.factor_name.as_str(), node)) - .collect::>(); - let mut selected = metrics - .iter() - .filter(|metric| metric.decision != "reject") - .filter(|metric| metric.runtime_pass_through_penalty < 8.0) - .map(|metric| { - let state_node = state_by_factor.get(metric.factor_name.as_str()); - let visits = state_node.map(|node| node.visits).unwrap_or(1).max(1) as f64; - let average_reward = state_node - .map(|node| node.total_reward / visits) - .unwrap_or(metric.reward); - let ucb_priority = - average_reward + exploration_weight * (total_visits.ln() / visits).sqrt(); - MctsSelectedNode { - node_id: metric.id.clone(), - factor_name: metric.factor_name.clone(), - selected_dimension: metric.selected_dimension.clone(), - proposed_mutation: proposed_mutation(&metric.selected_dimension), - reward: metric.reward, - ucb_priority, - } - }) - .collect::>(); - selected.sort_by(|lhs, rhs| rhs.ucb_priority.total_cmp(&lhs.ucb_priority)); - selected.truncate(12); - - MctsExpansionPlan { - version, - mode: "single_run_ucb_planner", - target: target.to_string(), - side, - exploration_weight, - selected_nodes: selected, - note: "MCTS state accumulates leaf visits and backpropagates leaf rewards through recorded parent lineage; this plan selects branches for the next bounded search run with UCB priority.", - } + let checkpoint = formula_mcts_checkpoint(version, target, side, metrics, None, Vec::new()) + .expect("test Formula MCTS checkpoint"); + formula_mcts_expansion_plan(version, target, side, &checkpoint) + .expect("test Formula MCTS expansion plan") } fn proposed_mutation(selected_dimension: &str) -> &'static str { @@ -1720,7 +2008,7 @@ fn selected_dimension( fn subtree_frequency_state( target: &str, reports: &[AutoFactorReport], - prior_state: Option<&MctsSearchStateArtifact>, + prior_state: Option<&FormulaMctsCheckpoint>, llm_prior: Option<&LlmPriorSpec>, ) -> Vec { let mut counts: BTreeMap = prior_state @@ -2298,6 +2586,33 @@ mod tests { } } + fn checkpoint( + target: &str, + side: Option, + nodes: Vec, + subtree_frequencies: Vec, + ) -> FormulaMctsCheckpoint { + FormulaMctsCheckpoint { + version: FORMULA_MCTS_CHECKPOINT_VERSION.to_string(), + target: target.to_string(), + side, + selection_budget: FORMULA_MCTS_SELECTION_BUDGET, + nodes: std::iter::once(FormulaMctsCheckpointNode { + factor_name: FORMULA_MCTS_ROOT_ID.to_string(), + parent_name: None, + visits: 0, + total_reward: 0.0, + best_reward: None, + last_reward: 0.0, + selected_dimension: "root".to_string(), + last_decision: "root".to_string(), + }) + .chain(nodes) + .collect(), + subtree_frequencies, + } + } + #[test] fn structural_signature_normalizes_commutative_operands() { let lhs = FactorExpr::Add( @@ -2417,21 +2732,17 @@ mod tests { ..sample_report("current_safe_div") }; let signature = structural_signature(&expr); - let prior = MctsSearchStateArtifact { - version: ALPHA_SEARCH_ARTIFACT_VERSION.to_string(), - mode: "cumulative_ucb_state".to_string(), - target: "full_depth_settlement_executable_pnl".to_string(), - side: None, - total_visits: 0, - backpropagation_truncated_count: 0, - nodes: Vec::new(), - subtree_frequencies: vec![SubtreeFrequencyState { + let prior = checkpoint( + "full_depth_settlement_executable_pnl", + None, + Vec::new(), + vec![SubtreeFrequencyState { root_gene: "SafeDiv".to_string(), structural_signature: signature.clone(), depth: structural_depth(&expr), count: 2, }], - }; + ); let frequencies = subtree_frequency_state( "full_depth_settlement_executable_pnl", @@ -2594,7 +2905,7 @@ mod tests { let write = |root: &Path, side, report: &AutoFactorReport, - state: Option<&MctsSearchStateArtifact>| { + state: Option<&FormulaMctsCheckpoint>| { write_side_bound_alpha_search_artifacts_with_state_and_runtime_feedback( root, target, @@ -2635,16 +2946,7 @@ mod tests { assert!(matches!(err, AlphaSearchArtifactError::IdentityMismatch(_))); assert!(!pooled_root.join(target).exists()); - let wrong_side_state = MctsSearchStateArtifact { - version: SIDE_BOUND_ALPHA_SEARCH_ARTIFACT_VERSION.to_string(), - mode: "cumulative_ucb_state".to_string(), - target: target.to_string(), - side: Some(ReviewSide::Down), - total_visits: 0, - backpropagation_truncated_count: 0, - nodes: Vec::new(), - subtree_frequencies: Vec::new(), - }; + let wrong_side_state = checkpoint(target, Some(ReviewSide::Down), Vec::new(), Vec::new()); let mismatch_root = tmp.join("mismatch"); let err = write(&mismatch_root, ReviewSide::Up, &up, Some(&wrong_side_state)) .expect_err("wrong-side prior state must fail closed"); @@ -2862,25 +3164,25 @@ mod tests { reason: "passed".to_string(), parent_name: None, }; - let prior = MctsSearchStateArtifact { - version: ALPHA_SEARCH_ARTIFACT_VERSION.to_string(), - mode: "cumulative_ucb_state".to_string(), - target: "full_depth_settlement_executable_pnl".to_string(), - side: None, - total_visits: 3, - backpropagation_truncated_count: 0, - nodes: vec![MctsSearchStateNode { + let mut prior = checkpoint( + "full_depth_settlement_executable_pnl", + None, + vec![FormulaMctsCheckpointNode { factor_name: "auto_settlement_conservative_settlement_edge".to_string(), parent_name: None, visits: 3, total_reward: 6.0, - best_reward: 2.0, + best_reward: Some(2.0), last_reward: 2.0, selected_dimension: "exploit".to_string(), last_decision: "candidate".to_string(), }], - subtree_frequencies: Vec::new(), - }; + Vec::new(), + ); + let root = prior.nodes.first_mut().expect("checkpoint root"); + root.visits = 3; + root.total_reward = 6.0; + root.best_reward = Some(2.0); write_alpha_search_artifacts_with_state( &tmp, @@ -3007,7 +3309,7 @@ mod tests { } #[test] - fn mcts_state_counts_cycle_truncated_backpropagation() { + fn formula_mcts_checkpoint_rejects_cyclic_lineage() { let runtime_avoidances = Vec::new(); let mut report = sample_report("cycle_a"); report.parent_name = Some("cycle_b".to_string()); @@ -3018,38 +3320,146 @@ mod tests { None, &Vec::new(), )]; - let prior = MctsSearchStateArtifact { - version: ALPHA_SEARCH_ARTIFACT_VERSION.to_string(), - mode: "cumulative_ucb_state".to_string(), - target: "full_depth_settlement_executable_pnl".to_string(), - side: None, - total_visits: 0, - backpropagation_truncated_count: 0, - nodes: vec![MctsSearchStateNode { + let prior = checkpoint( + "full_depth_settlement_executable_pnl", + None, + vec![FormulaMctsCheckpointNode { factor_name: "cycle_b".to_string(), parent_name: Some("cycle_a".to_string()), visits: 0, total_reward: 0.0, - best_reward: f64::NEG_INFINITY, + best_reward: None, last_reward: 0.0, selected_dimension: "exploit".to_string(), last_decision: "candidate".to_string(), }], - subtree_frequencies: Vec::new(), - }; + Vec::new(), + ); - let state = mcts_search_state( + let err = formula_mcts_checkpoint( ALPHA_SEARCH_ARTIFACT_VERSION, "full_depth_settlement_executable_pnl", None, &metrics, Some(&prior), Vec::new(), + ) + .expect_err("cyclic Formula candidate lineage must fail closed"); + + assert!(matches!(err, AlphaSearchArtifactError::IdentityMismatch(_))); + } + + #[test] + fn formula_mcts_checkpoint_resume_matches_uninterrupted_run() { + let runtime_avoidances = Vec::new(); + let root = sample_report("resume_root"); + let mut child = sample_report("resume_child"); + child.parent_name = Some(root.name.clone()); + let metrics = [root, child] + .iter() + .enumerate() + .map(|(idx, report)| node_metric(idx, report, &runtime_avoidances, None, &Vec::new())) + .collect::>(); + let uninterrupted = formula_mcts_checkpoint( + ALPHA_SEARCH_ARTIFACT_VERSION, + "full_depth_settlement_executable_pnl", + None, + &metrics, + None, + Vec::new(), + ) + .expect("uninterrupted checkpoint"); + let first = formula_mcts_checkpoint( + ALPHA_SEARCH_ARTIFACT_VERSION, + "full_depth_settlement_executable_pnl", + None, + &metrics[..1], + None, + Vec::new(), + ) + .expect("first checkpoint"); + let resumed = formula_mcts_checkpoint( + ALPHA_SEARCH_ARTIFACT_VERSION, + "full_depth_settlement_executable_pnl", + None, + &metrics[1..], + Some(&first), + Vec::new(), + ) + .expect("resumed checkpoint"); + + assert_eq!( + serde_json::to_vec(&resumed).expect("serialize resumed checkpoint"), + serde_json::to_vec(&uninterrupted).expect("serialize uninterrupted checkpoint") ); + } - assert_eq!(state.backpropagation_truncated_count, 1); - assert!(state.nodes.iter().any(|node| node.factor_name == "cycle_a")); - assert!(state.nodes.iter().any(|node| node.factor_name == "cycle_b")); + #[test] + fn formula_mcts_checkpoint_rejects_forged_target_and_legacy_state() { + let forged = checkpoint("other_target", None, Vec::new(), Vec::new()); + let err = formula_mcts_checkpoint( + ALPHA_SEARCH_ARTIFACT_VERSION, + "full_depth_settlement_executable_pnl", + None, + &[], + Some(&forged), + Vec::new(), + ) + .expect_err("forged target must not resume Formula search"); + assert!(matches!(err, AlphaSearchArtifactError::IdentityMismatch(_))); + + let mut altered_budget = checkpoint( + "full_depth_settlement_executable_pnl", + None, + Vec::new(), + Vec::new(), + ); + altered_budget.selection_budget = FORMULA_MCTS_SELECTION_BUDGET + 1; + let err = formula_mcts_checkpoint( + ALPHA_SEARCH_ARTIFACT_VERSION, + "full_depth_settlement_executable_pnl", + None, + &[], + Some(&altered_budget), + Vec::new(), + ) + .expect_err("altered selection budget must not resume Formula search"); + assert!(matches!(err, AlphaSearchArtifactError::IdentityMismatch(_))); + + let mut reordered = checkpoint( + "full_depth_settlement_executable_pnl", + None, + vec![FormulaMctsCheckpointNode { + factor_name: "forged_candidate".to_string(), + parent_name: None, + visits: 1, + total_reward: 1.0, + best_reward: Some(1.0), + last_reward: 1.0, + selected_dimension: "exploit".to_string(), + last_decision: "candidate".to_string(), + }], + Vec::new(), + ); + reordered.nodes.swap(0, 1); + let err = mcts_search_state_projection(&reordered) + .expect_err("reordered checkpoint must not project a wrong root budget"); + assert!(matches!(err, AlphaSearchArtifactError::IdentityMismatch(_))); + + let temp = tempfile::tempdir().expect("temporary legacy state"); + let path = temp.path().join("mcts-state.json"); + std::fs::write( + &path, + r#"{"version":"alpha_search_artifacts_v1","target":"full_depth_settlement_executable_pnl"}"#, + ) + .expect("write legacy state"); + let err = read_formula_mcts_checkpoint(&path) + .expect_err("legacy projection must not be reinterpreted as a checkpoint"); + assert!(matches!( + err, + AlphaSearchArtifactError::LegacyCheckpointVersion(ref version) + if version == ALPHA_SEARCH_ARTIFACT_VERSION + )); } #[test] diff --git a/rust_hft/prediction-markets/crates/ploy-research/src/bin/monday-prediction-evaluator.rs b/rust_hft/prediction-markets/crates/ploy-research/src/bin/monday-prediction-evaluator.rs index e73d51904..4c12d0313 100644 --- a/rust_hft/prediction-markets/crates/ploy-research/src/bin/monday-prediction-evaluator.rs +++ b/rust_hft/prediction-markets/crates/ploy-research/src/bin/monday-prediction-evaluator.rs @@ -27,7 +27,7 @@ use ploy_research::{ format_settlement_probability_walk_forward_report, format_trade_formation_v1_report, liquidity_gate_v1_with_deribit_and_pm_books, liquidity_gated_alpha_v1_with_deribit_and_pm_books, load_research_snapshot, - mine_domain_autofactors_from_v2_with_guidance, read_mcts_search_state, + mine_domain_autofactors_from_v2_with_guidance, read_formula_mcts_checkpoint, review_fillability_v1_with_deribit_and_pm_books, review_repricing_ic_with_deribit_and_pm_books, review_trade_formation_v1_with_deribit_and_pm_books, split_reprice_rows_by_event_cohort, validate_prediction_research_prior, validate_snapshot_request_coverage, @@ -115,6 +115,7 @@ struct FullDepthExecutionArtifact<'a> { #[derive(serde::Serialize)] struct RepricePilotSearchArtifact { summary: AlphaSearchArtifactSummary, + formula_mcts_checkpoint_sha256: String, mcts_state_sha256: String, mcts_expansion_plan_sha256: String, } @@ -210,6 +211,7 @@ fn validate_reprice_pilot_config( "--alpha-search-plan-json", "--alpha-search-llm-prior-json", "--alpha-search-state-json", + "--formula-mcts-checkpoint-json", "--alpha-zoo-snapshot-json", "--candidate-strategy-replay-json", ] { @@ -558,6 +560,9 @@ fn run_reprice_pilot_10s( .map_err(|error| format!("evaluate frozen reprice selection: {error}"))?; let search_dir = Path::new(&summary.output_dir); let search = RepricePilotSearchArtifact { + formula_mcts_checkpoint_sha256: sha256_file( + &search_dir.join("formula-mcts-checkpoint.json"), + )?, mcts_state_sha256: sha256_file(&search_dir.join("mcts-state.json"))?, mcts_expansion_plan_sha256: sha256_file(&search_dir.join("mcts-expansion-plan.json"))?, summary, @@ -1442,6 +1447,12 @@ async fn main() { eprintln!("alpha search typed LLM prior loaded from {path}"); } let alpha_search_state_json = flag_value(&args, "--alpha-search-state-json"); + if let Some(path) = alpha_search_state_json.as_deref() { + panic!( + "legacy alpha search state `{path}` is not resumable; use --formula-mcts-checkpoint-json" + ); + } + let formula_mcts_checkpoint_json = flag_value(&args, "--formula-mcts-checkpoint-json"); let alpha_zoo_snapshot_json = flag_value(&args, "--alpha-zoo-snapshot-json"); let data_quality_mode = parse_data_quality_mode(flag_value(&args, "--data-quality-mode")); let require_deribit = flag_present(&args, "--require-deribit"); @@ -2002,12 +2013,12 @@ async fn main() { .as_deref() .map(alpha_search_plan_factor_names) .unwrap_or_default(); - let mcts_state = alpha_search_state_json.as_deref().map(|path| { - read_mcts_search_state(path) - .unwrap_or_else(|err| panic!("read alpha search MCTS state JSON {path} failed: {err}")) + let mcts_state = formula_mcts_checkpoint_json.as_deref().map(|path| { + read_formula_mcts_checkpoint(path) + .unwrap_or_else(|err| panic!("read Formula MCTS checkpoint JSON {path} failed: {err}")) }); - if let Some(path) = alpha_search_state_json.as_deref() { - eprintln!("alpha search cumulative MCTS state loaded from {path}"); + if let Some(path) = formula_mcts_checkpoint_json.as_deref() { + eprintln!("Formula MCTS checkpoint loaded from {path}"); } let alpha_zoo = alpha_zoo_snapshot_json .as_deref() diff --git a/rust_hft/prediction-markets/crates/ploy-research/src/lib.rs b/rust_hft/prediction-markets/crates/ploy-research/src/lib.rs index 608a6cee6..5593cd418 100644 --- a/rust_hft/prediction-markets/crates/ploy-research/src/lib.rs +++ b/rust_hft/prediction-markets/crates/ploy-research/src/lib.rs @@ -109,12 +109,13 @@ pub fn crate_marker() -> &'static str { // factor registry uses the same type internally, but does not re-export its own // `factors_new::Regime` alias. pub use alpha_search::{ - read_mcts_search_state, root_gene, write_alpha_search_artifacts, + read_formula_mcts_checkpoint, read_mcts_search_state, root_gene, write_alpha_search_artifacts, write_alpha_search_artifacts_with_state, write_alpha_search_artifacts_with_state_and_runtime_feedback, write_side_bound_alpha_search_artifacts_with_state_and_runtime_feedback, AlphaSearchArtifactError, AlphaSearchArtifactSummary, AlphaSearchRuntimeFeedback, - AlphaZooEntry, AlphaZooSnapshot, MctsSearchStateArtifact, MctsSearchStateNode, + AlphaZooEntry, AlphaZooSnapshot, FormulaMctsCheckpoint, FormulaMctsCheckpointNode, + MctsSearchStateArtifact, MctsSearchStateNode, FORMULA_MCTS_CHECKPOINT_VERSION, SIDE_BOUND_ALPHA_SEARCH_ARTIFACT_VERSION, }; pub use attribution::{factor_pnl, regime_pnl, AttributionReport, RegimePnl}; diff --git a/rust_hft/prediction-markets/docs/ALPHA_FACTOR_SEARCH_CICD.md b/rust_hft/prediction-markets/docs/ALPHA_FACTOR_SEARCH_CICD.md index cf22fa33e..177d0e39a 100644 --- a/rust_hft/prediction-markets/docs/ALPHA_FACTOR_SEARCH_CICD.md +++ b/rust_hft/prediction-markets/docs/ALPHA_FACTOR_SEARCH_CICD.md @@ -756,27 +756,20 @@ Current implementation status: a candidate cap to keep CI runs bounded. - Implemented: workflow upload path for the artifact bundle through both Factor Walk-Forward V2 workflows. -- Implemented: MCTS control artifacts, `mcts-state.json` and - `mcts-expansion-plan.json`. The state artifact stores explicit factor - parent lineage, accumulates leaf visits, backpropagates leaf rewards through - ancestor nodes across runs, and persists structural subtree frequencies - across runs. The expansion plan ranks non-rejected current-run nodes with a - UCB-style priority using that cumulative state. - `mcts-state.json.backpropagation_truncated_count > 0` means reward - propagation hit a defensive stop before reaching a root node. Inspect - `parent_name` lineage for cycles first; if there is no cycle, check whether - the search state graph has grown beyond the expected bounded chain size. CI - warnings report only newly observed truncations while including the - cumulative artifact count. +- Implemented: `formula-mcts-checkpoint.json` is the only resumable Formula + search state. It records candidate parent lineage, reward statistics, and + the existing 12-branch selection budget around the shared UCT kernel. + `mcts-state.json` and `mcts-expansion-plan.json` are read-only projections. + Invalid lineage, reordered roots, altered budgets, and legacy state versions + fail closed rather than being reinterpreted. - Implemented: `factor_walk_forward_v2 --alpha-search-plan-json ` can consume a prior `mcts-expansion-plan.json` and generate extra `mcts_*` guided mutations for selected branches. The Factor Walk-Forward workflows expose this as `options_json.alpha_search_plan_json`. -- Implemented: `factor_walk_forward_v2 --alpha-search-state-json ` can - consume a prior `mcts-state.json`; when a prior alpha-search artifact is - downloaded via `options_json.alpha_search_plan_run_id`, the workflows pass - its `mcts-state.json` automatically when present. This carries both MCTS node - rewards and structural subtree frequency counts into the next run. +- Implemented: `monday-prediction-evaluator + --formula-mcts-checkpoint-json ` resumes only from the new checkpoint. + `--alpha-search-state-json` is a legacy projection input and is rejected + with an explicit migration diagnostic. - Implemented: `factor_walk_forward_v2 --alpha-search-llm-prior-json ` accepts a typed LLM-prior JSON file with bounded mutation requests. The Rust layer compiles those requests into existing `FactorExpr` candidates only when From 54c933cca8fdb4f2b267be9456669dc391828c3e Mon Sep 17 00:00:00 2001 From: Sonic Shih Date: Wed, 22 Jul 2026 16:10:03 +0800 Subject: [PATCH 2/2] fix(research): validate Formula state before output --- .../polymarket-btc-5m.example.json | 2 +- .../polymarket-sol-5m.example.json | 2 +- .../src/bin/monday-prediction-evaluator.rs | 16 ++++++++-------- .../docs/ALPHA_FACTOR_SEARCH_CICD.md | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rust_hft/prediction-markets/config/research_missions/polymarket-btc-5m.example.json b/rust_hft/prediction-markets/config/research_missions/polymarket-btc-5m.example.json index 8ca305086..68c82cf76 100644 --- a/rust_hft/prediction-markets/config/research_missions/polymarket-btc-5m.example.json +++ b/rust_hft/prediction-markets/config/research_missions/polymarket-btc-5m.example.json @@ -15,7 +15,7 @@ "horizon": "5m", "time_cohort_boundary_ms": 0, "prompt_snapshot_id": "sha256:2b55ba0e724dfc9f5a040911e397058a09c6229e42064e4998739d617b368dcb", - "search_policy_snapshot_id": "sha256:e98b091da59ca2878c79d817b5d774cc8f3591384730eea93b76499f0c0eea79", + "search_policy_snapshot_id": "sha256:8a4469dabb68129840c7b46b454f0d2fe42b9530b2f63b0a72e80cbb1b9f67f3", "search_budget": { "max_candidates": 6, "max_llm_calls": 2, diff --git a/rust_hft/prediction-markets/config/research_missions/polymarket-sol-5m.example.json b/rust_hft/prediction-markets/config/research_missions/polymarket-sol-5m.example.json index eed10efa3..3a1ac6cac 100644 --- a/rust_hft/prediction-markets/config/research_missions/polymarket-sol-5m.example.json +++ b/rust_hft/prediction-markets/config/research_missions/polymarket-sol-5m.example.json @@ -15,7 +15,7 @@ "horizon": "5m", "time_cohort_boundary_ms": 0, "prompt_snapshot_id": "sha256:0816ebccf4c75ee6bdcfe315b253c84ddf1808a38687ebe3b924b87bd72a52a9", - "search_policy_snapshot_id": "sha256:e98b091da59ca2878c79d817b5d774cc8f3591384730eea93b76499f0c0eea79", + "search_policy_snapshot_id": "sha256:8a4469dabb68129840c7b46b454f0d2fe42b9530b2f63b0a72e80cbb1b9f67f3", "search_budget": { "max_candidates": 6, "max_llm_calls": 2, diff --git a/rust_hft/prediction-markets/crates/ploy-research/src/bin/monday-prediction-evaluator.rs b/rust_hft/prediction-markets/crates/ploy-research/src/bin/monday-prediction-evaluator.rs index 4c12d0313..5c56e259a 100644 --- a/rust_hft/prediction-markets/crates/ploy-research/src/bin/monday-prediction-evaluator.rs +++ b/rust_hft/prediction-markets/crates/ploy-research/src/bin/monday-prediction-evaluator.rs @@ -568,7 +568,7 @@ fn run_reprice_pilot_10s( summary, }; let artifact = RepricePilotArtifact { - schema_version: "monday.polymarket.reprice_pilot.v1", + schema_version: "monday.polymarket.reprice_pilot.v2", non_finite_floats: "null", context, status: "pilot_not_promotable", @@ -1453,6 +1453,13 @@ async fn main() { ); } let formula_mcts_checkpoint_json = flag_value(&args, "--formula-mcts-checkpoint-json"); + let mcts_state = formula_mcts_checkpoint_json.as_deref().map(|path| { + read_formula_mcts_checkpoint(path) + .unwrap_or_else(|err| panic!("read Formula MCTS checkpoint JSON {path} failed: {err}")) + }); + if let Some(path) = formula_mcts_checkpoint_json.as_deref() { + eprintln!("Formula MCTS checkpoint loaded from {path}"); + } let alpha_zoo_snapshot_json = flag_value(&args, "--alpha-zoo-snapshot-json"); let data_quality_mode = parse_data_quality_mode(flag_value(&args, "--data-quality-mode")); let require_deribit = flag_present(&args, "--require-deribit"); @@ -2013,13 +2020,6 @@ async fn main() { .as_deref() .map(alpha_search_plan_factor_names) .unwrap_or_default(); - let mcts_state = formula_mcts_checkpoint_json.as_deref().map(|path| { - read_formula_mcts_checkpoint(path) - .unwrap_or_else(|err| panic!("read Formula MCTS checkpoint JSON {path} failed: {err}")) - }); - if let Some(path) = formula_mcts_checkpoint_json.as_deref() { - eprintln!("Formula MCTS checkpoint loaded from {path}"); - } let alpha_zoo = alpha_zoo_snapshot_json .as_deref() .map(read_alpha_zoo_snapshot); diff --git a/rust_hft/prediction-markets/docs/ALPHA_FACTOR_SEARCH_CICD.md b/rust_hft/prediction-markets/docs/ALPHA_FACTOR_SEARCH_CICD.md index 177d0e39a..14420eede 100644 --- a/rust_hft/prediction-markets/docs/ALPHA_FACTOR_SEARCH_CICD.md +++ b/rust_hft/prediction-markets/docs/ALPHA_FACTOR_SEARCH_CICD.md @@ -768,7 +768,7 @@ Current implementation status: expose this as `options_json.alpha_search_plan_json`. - Implemented: `monday-prediction-evaluator --formula-mcts-checkpoint-json ` resumes only from the new checkpoint. - `--alpha-search-state-json` is a legacy projection input and is rejected + `--alpha-search-state-json` is a legacy state input and is rejected with an explicit migration diagnostic. - Implemented: `factor_walk_forward_v2 --alpha-search-llm-prior-json ` accepts a typed LLM-prior JSON file with bounded mutation requests. The Rust