add belief revision and data gaps tracking to game engine
This commit is contained in:
parent
3c15d443d3
commit
80b96497fd
60
app/agents/beliefs.py
Normal file
60
app/agents/beliefs.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""Belief revision helpers for multi-round negotiation."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, Iterable, List, Optional
|
||||||
|
|
||||||
|
from .base import AgentAction
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BeliefRevisionResult:
|
||||||
|
consensus_action: Optional[AgentAction]
|
||||||
|
consensus_confidence: float
|
||||||
|
conflicts: List[str]
|
||||||
|
notes: Dict[str, object]
|
||||||
|
|
||||||
|
|
||||||
|
def revise_beliefs(belief_updates: Dict[str, "BeliefUpdate"], default_action: AgentAction) -> BeliefRevisionResult:
|
||||||
|
action_votes: Dict[AgentAction, int] = {}
|
||||||
|
reasons: Dict[str, object] = {}
|
||||||
|
for agent, update in belief_updates.items():
|
||||||
|
belief = getattr(update, "belief", {}) or {}
|
||||||
|
action_value = belief.get("action") if isinstance(belief, dict) else None
|
||||||
|
try:
|
||||||
|
action = AgentAction(action_value) if action_value else None
|
||||||
|
except ValueError:
|
||||||
|
action = None
|
||||||
|
if action:
|
||||||
|
action_votes[action] = action_votes.get(action, 0) + 1
|
||||||
|
reasons[agent] = belief
|
||||||
|
|
||||||
|
consensus_action = None
|
||||||
|
consensus_confidence = 0.0
|
||||||
|
conflicts: List[str] = []
|
||||||
|
if action_votes:
|
||||||
|
total_votes = sum(action_votes.values())
|
||||||
|
consensus_action = max(action_votes.items(), key=lambda kv: kv[1])[0]
|
||||||
|
consensus_confidence = action_votes[consensus_action] / total_votes if total_votes else 0.0
|
||||||
|
if len(action_votes) > 1:
|
||||||
|
conflicts = [action.name for action in action_votes.keys() if action is not consensus_action]
|
||||||
|
|
||||||
|
if consensus_action is None:
|
||||||
|
consensus_action = default_action
|
||||||
|
|
||||||
|
notes = {
|
||||||
|
"votes": {action.value: count for action, count in action_votes.items()},
|
||||||
|
"reasons": reasons,
|
||||||
|
}
|
||||||
|
return BeliefRevisionResult(
|
||||||
|
consensus_action=consensus_action,
|
||||||
|
consensus_confidence=consensus_confidence,
|
||||||
|
conflicts=conflicts,
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# avoid circular import typing
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from .game import BeliefUpdate
|
||||||
@ -8,6 +8,7 @@ from typing import Dict, Iterable, List, Mapping, Optional, Tuple
|
|||||||
from .base import Agent, AgentAction, AgentContext, UtilityMatrix
|
from .base import Agent, AgentAction, AgentContext, UtilityMatrix
|
||||||
from .departments import DepartmentContext, DepartmentDecision, DepartmentManager
|
from .departments import DepartmentContext, DepartmentDecision, DepartmentManager
|
||||||
from .registry import weight_map
|
from .registry import weight_map
|
||||||
|
from .beliefs import BeliefRevisionResult, revise_beliefs
|
||||||
from .risk import RiskAgent, RiskRecommendation
|
from .risk import RiskAgent, RiskRecommendation
|
||||||
from .protocols import (
|
from .protocols import (
|
||||||
DialogueMessage,
|
DialogueMessage,
|
||||||
@ -69,6 +70,7 @@ class Decision:
|
|||||||
rounds: List[RoundSummary] = field(default_factory=list)
|
rounds: List[RoundSummary] = field(default_factory=list)
|
||||||
risk_assessment: Optional[RiskAssessment] = None
|
risk_assessment: Optional[RiskAssessment] = None
|
||||||
belief_updates: Dict[str, BeliefUpdate] = field(default_factory=dict)
|
belief_updates: Dict[str, BeliefUpdate] = field(default_factory=dict)
|
||||||
|
belief_revision: Optional[BeliefRevisionResult] = None
|
||||||
|
|
||||||
|
|
||||||
def compute_utilities(agents: Iterable[Agent], context: AgentContext) -> UtilityMatrix:
|
def compute_utilities(agents: Iterable[Agent], context: AgentContext) -> UtilityMatrix:
|
||||||
@ -336,6 +338,13 @@ def decide(
|
|||||||
action,
|
action,
|
||||||
department_votes,
|
department_votes,
|
||||||
)
|
)
|
||||||
|
belief_revision = revise_beliefs(belief_updates, exec_action)
|
||||||
|
execution_round.notes.setdefault("consensus_action", belief_revision.consensus_action.value)
|
||||||
|
execution_round.notes.setdefault("consensus_confidence", belief_revision.consensus_confidence)
|
||||||
|
if belief_revision.conflicts:
|
||||||
|
execution_round.notes.setdefault("conflicts", belief_revision.conflicts)
|
||||||
|
if belief_revision.notes:
|
||||||
|
execution_round.notes.setdefault("belief_notes", belief_revision.notes)
|
||||||
return Decision(
|
return Decision(
|
||||||
action=action,
|
action=action,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
@ -348,6 +357,7 @@ def decide(
|
|||||||
rounds=rounds,
|
rounds=rounds,
|
||||||
risk_assessment=risk_assessment,
|
risk_assessment=risk_assessment,
|
||||||
belief_updates=belief_updates,
|
belief_updates=belief_updates,
|
||||||
|
belief_revision=belief_revision,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
51
app/agents/scopes.py
Normal file
51
app/agents/scopes.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""Scope mappings for different game structures."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, Iterable, Set
|
||||||
|
|
||||||
|
from .protocols import GameStructure
|
||||||
|
|
||||||
|
|
||||||
|
_GAME_SCOPE_MAP: Dict[GameStructure, Set[str]] = {
|
||||||
|
GameStructure.REPEATED: {
|
||||||
|
"daily.close",
|
||||||
|
"daily.open",
|
||||||
|
"daily.high",
|
||||||
|
"daily.low",
|
||||||
|
"daily_basic.turnover_rate",
|
||||||
|
"daily_basic.turnover_rate_f",
|
||||||
|
},
|
||||||
|
GameStructure.SIGNALING: {
|
||||||
|
"daily.close",
|
||||||
|
"daily.high",
|
||||||
|
"daily_basic.turnover_rate",
|
||||||
|
"daily_basic.volume_ratio",
|
||||||
|
"factors.sent_momentum",
|
||||||
|
"factors.sent_market",
|
||||||
|
},
|
||||||
|
GameStructure.BAYESIAN: {
|
||||||
|
"daily.close",
|
||||||
|
"daily_basic.turnover_rate",
|
||||||
|
"factors.mom_20",
|
||||||
|
"factors.mom_60",
|
||||||
|
"factors.val_multiscore",
|
||||||
|
"factors.sent_divergence",
|
||||||
|
},
|
||||||
|
GameStructure.CUSTOM: {
|
||||||
|
"factors.risk_penalty",
|
||||||
|
"factors.turn_20",
|
||||||
|
"factors.volat_20",
|
||||||
|
"daily_basic.turnover_rate",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scope_for_structures(structures: Iterable[GameStructure]) -> Set[str]:
|
||||||
|
scope: Set[str] = set()
|
||||||
|
for structure in structures:
|
||||||
|
scope.update(_GAME_SCOPE_MAP.get(structure, set()))
|
||||||
|
return scope
|
||||||
|
|
||||||
|
|
||||||
|
def registered_structures() -> Dict[GameStructure, Set[str]]:
|
||||||
|
return {key: set(values) for key, values in _GAME_SCOPE_MAP.items()}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple
|
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple
|
||||||
@ -9,7 +10,8 @@ from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple
|
|||||||
from app.agents.base import AgentAction, AgentContext
|
from app.agents.base import AgentAction, AgentContext
|
||||||
from app.agents.departments import DepartmentManager
|
from app.agents.departments import DepartmentManager
|
||||||
from app.agents.game import Decision, decide, target_weight_for_action
|
from app.agents.game import Decision, decide, target_weight_for_action
|
||||||
from app.agents.protocols import round_to_dict
|
from app.agents.protocols import GameStructure, round_to_dict
|
||||||
|
from app.agents.scopes import scope_for_structures
|
||||||
from app.llm.metrics import record_decision as metrics_record_decision
|
from app.llm.metrics import record_decision as metrics_record_decision
|
||||||
from app.agents.registry import default_agents
|
from app.agents.registry import default_agents
|
||||||
from app.data.schema import initialize_database
|
from app.data.schema import initialize_database
|
||||||
@ -56,6 +58,7 @@ class BtConfig:
|
|||||||
universe: List[str]
|
universe: List[str]
|
||||||
params: Dict[str, float]
|
params: Dict[str, float]
|
||||||
method: str = "nash"
|
method: str = "nash"
|
||||||
|
game_structures: Optional[List[GameStructure]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -72,6 +75,7 @@ class BacktestResult:
|
|||||||
nav_series: List[Dict[str, float]] = field(default_factory=list)
|
nav_series: List[Dict[str, float]] = field(default_factory=list)
|
||||||
trades: List[Dict[str, str]] = field(default_factory=list)
|
trades: List[Dict[str, str]] = field(default_factory=list)
|
||||||
risk_events: List[Dict[str, object]] = field(default_factory=list)
|
risk_events: List[Dict[str, object]] = field(default_factory=list)
|
||||||
|
data_gaps: List[Dict[str, object]] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -137,7 +141,19 @@ class BacktestEngine:
|
|||||||
"factors.volat_20",
|
"factors.volat_20",
|
||||||
"factors.turn_20",
|
"factors.turn_20",
|
||||||
}
|
}
|
||||||
self.required_fields = sorted(base_scope | department_scope)
|
selected_structures = (
|
||||||
|
cfg.game_structures
|
||||||
|
if cfg.game_structures
|
||||||
|
else [
|
||||||
|
GameStructure.REPEATED,
|
||||||
|
GameStructure.SIGNALING,
|
||||||
|
GameStructure.BAYESIAN,
|
||||||
|
GameStructure.CUSTOM,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.game_structures = list(dict.fromkeys(selected_structures))
|
||||||
|
structure_scope = scope_for_structures(self.game_structures)
|
||||||
|
self.required_fields = sorted(base_scope | department_scope | structure_scope)
|
||||||
|
|
||||||
def load_market_data(self, trade_date: date) -> Mapping[str, Dict[str, Any]]:
|
def load_market_data(self, trade_date: date) -> Mapping[str, Dict[str, Any]]:
|
||||||
"""Load per-stock feature vectors and context slices for the trade date."""
|
"""Load per-stock feature vectors and context slices for the trade date."""
|
||||||
@ -152,6 +168,19 @@ class BacktestEngine:
|
|||||||
self.required_fields,
|
self.required_fields,
|
||||||
auto_refresh=False # 避免回测时触发自动补数
|
auto_refresh=False # 避免回测时触发自动补数
|
||||||
)
|
)
|
||||||
|
missing_fields = [
|
||||||
|
field
|
||||||
|
for field in self.required_fields
|
||||||
|
if scope_values.get(field) is None
|
||||||
|
]
|
||||||
|
derived_fields: List[str] = []
|
||||||
|
if missing_fields:
|
||||||
|
LOGGER.debug(
|
||||||
|
"字段缺失,使用回退或派生数据 ts_code=%s fields=%s",
|
||||||
|
ts_code,
|
||||||
|
missing_fields,
|
||||||
|
extra=LOG_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
closes = self.data_broker.fetch_series(
|
closes = self.data_broker.fetch_series(
|
||||||
"daily",
|
"daily",
|
||||||
@ -166,17 +195,21 @@ class BacktestEngine:
|
|||||||
mom5 = scope_values.get("factors.mom_5")
|
mom5 = scope_values.get("factors.mom_5")
|
||||||
if mom5 is None and len(close_values) >= 5:
|
if mom5 is None and len(close_values) >= 5:
|
||||||
mom5 = momentum(close_values, 5)
|
mom5 = momentum(close_values, 5)
|
||||||
|
derived_fields.append("factors.mom_5")
|
||||||
mom20 = scope_values.get("factors.mom_20")
|
mom20 = scope_values.get("factors.mom_20")
|
||||||
if mom20 is None and len(close_values) >= 20:
|
if mom20 is None and len(close_values) >= 20:
|
||||||
mom20 = momentum(close_values, 20)
|
mom20 = momentum(close_values, 20)
|
||||||
|
derived_fields.append("factors.mom_20")
|
||||||
|
|
||||||
mom60 = scope_values.get("factors.mom_60")
|
mom60 = scope_values.get("factors.mom_60")
|
||||||
if mom60 is None and len(close_values) >= 60:
|
if mom60 is None and len(close_values) >= 60:
|
||||||
mom60 = momentum(close_values, 60)
|
mom60 = momentum(close_values, 60)
|
||||||
|
derived_fields.append("factors.mom_60")
|
||||||
|
|
||||||
volat20 = scope_values.get("factors.volat_20")
|
volat20 = scope_values.get("factors.volat_20")
|
||||||
if volat20 is None and len(close_values) >= 2:
|
if volat20 is None and len(close_values) >= 2:
|
||||||
volat20 = volatility(close_values, 20)
|
volat20 = volatility(close_values, 20)
|
||||||
|
derived_fields.append("factors.volat_20")
|
||||||
|
|
||||||
turnover_series = self.data_broker.fetch_series(
|
turnover_series = self.data_broker.fetch_series(
|
||||||
"daily_basic",
|
"daily_basic",
|
||||||
@ -191,9 +224,11 @@ class BacktestEngine:
|
|||||||
turn20 = scope_values.get("factors.turn_20")
|
turn20 = scope_values.get("factors.turn_20")
|
||||||
if turn20 is None and turnover_values:
|
if turn20 is None and turnover_values:
|
||||||
turn20 = rolling_mean(turnover_values, 20)
|
turn20 = rolling_mean(turnover_values, 20)
|
||||||
|
derived_fields.append("factors.turn_20")
|
||||||
turn5 = scope_values.get("factors.turn_5")
|
turn5 = scope_values.get("factors.turn_5")
|
||||||
if turn5 is None and len(turnover_values) >= 5:
|
if turn5 is None and len(turnover_values) >= 5:
|
||||||
turn5 = rolling_mean(turnover_values, 5)
|
turn5 = rolling_mean(turnover_values, 5)
|
||||||
|
derived_fields.append("factors.turn_5")
|
||||||
|
|
||||||
if mom20 is None:
|
if mom20 is None:
|
||||||
mom20 = 0.0
|
mom20 = 0.0
|
||||||
@ -217,14 +252,24 @@ class BacktestEngine:
|
|||||||
val_pe = scope_values.get("factors.val_pe_score")
|
val_pe = scope_values.get("factors.val_pe_score")
|
||||||
if val_pe is None:
|
if val_pe is None:
|
||||||
val_pe = _valuation_score(scope_values.get("daily_basic.pe"), scale=12.0)
|
val_pe = _valuation_score(scope_values.get("daily_basic.pe"), scale=12.0)
|
||||||
|
derived_fields.append("factors.val_pe_score")
|
||||||
|
|
||||||
val_pb = scope_values.get("factors.val_pb_score")
|
val_pb = scope_values.get("factors.val_pb_score")
|
||||||
if val_pb is None:
|
if val_pb is None:
|
||||||
val_pb = _valuation_score(scope_values.get("daily_basic.pb"), scale=2.5)
|
val_pb = _valuation_score(scope_values.get("daily_basic.pb"), scale=2.5)
|
||||||
|
derived_fields.append("factors.val_pb_score")
|
||||||
|
|
||||||
volume_ratio_score = scope_values.get("factors.volume_ratio_score")
|
volume_ratio_score = scope_values.get("factors.volume_ratio_score")
|
||||||
if volume_ratio_score is None:
|
if volume_ratio_score is None:
|
||||||
volume_ratio_score = _volume_ratio_score(scope_values.get("daily_basic.volume_ratio"))
|
volume_ratio_score = _volume_ratio_score(scope_values.get("daily_basic.volume_ratio"))
|
||||||
|
derived_fields.append("factors.volume_ratio_score")
|
||||||
|
if derived_fields:
|
||||||
|
LOGGER.debug(
|
||||||
|
"字段派生完成 ts_code=%s derived=%s",
|
||||||
|
ts_code,
|
||||||
|
derived_fields,
|
||||||
|
extra=LOG_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
sentiment_index = scope_values.get("news.sentiment_index", 0.0)
|
sentiment_index = scope_values.get("news.sentiment_index", 0.0)
|
||||||
heat_score = scope_values.get("news.heat_score", 0.0)
|
heat_score = scope_values.get("news.heat_score", 0.0)
|
||||||
@ -316,6 +361,8 @@ class BacktestEngine:
|
|||||||
"close_series": closes,
|
"close_series": closes,
|
||||||
"turnover_series": turnover_series,
|
"turnover_series": turnover_series,
|
||||||
"required_fields": self.required_fields,
|
"required_fields": self.required_fields,
|
||||||
|
"missing_fields": missing_fields,
|
||||||
|
"derived_fields": derived_fields,
|
||||||
}
|
}
|
||||||
|
|
||||||
feature_map[ts_code] = {
|
feature_map[ts_code] = {
|
||||||
@ -512,6 +559,8 @@ class BacktestEngine:
|
|||||||
price_map: Dict[str, float] = {}
|
price_map: Dict[str, float] = {}
|
||||||
decisions_map: Dict[str, Decision] = {}
|
decisions_map: Dict[str, Decision] = {}
|
||||||
feature_cache: Dict[str, Mapping[str, Any]] = {}
|
feature_cache: Dict[str, Mapping[str, Any]] = {}
|
||||||
|
missing_counts: Dict[str, int] = defaultdict(int)
|
||||||
|
derived_counts: Dict[str, int] = defaultdict(int)
|
||||||
for ts_code, context, decision in records:
|
for ts_code, context, decision in records:
|
||||||
features = context.features or {}
|
features = context.features or {}
|
||||||
if not isinstance(features, Mapping):
|
if not isinstance(features, Mapping):
|
||||||
@ -520,6 +569,12 @@ class BacktestEngine:
|
|||||||
scope_values = context.raw.get("scope_values") if context.raw else {}
|
scope_values = context.raw.get("scope_values") if context.raw else {}
|
||||||
if not isinstance(scope_values, Mapping):
|
if not isinstance(scope_values, Mapping):
|
||||||
scope_values = {}
|
scope_values = {}
|
||||||
|
raw_missing = context.raw.get("missing_fields") if context.raw else []
|
||||||
|
raw_derived = context.raw.get("derived_fields") if context.raw else []
|
||||||
|
for field in raw_missing or []:
|
||||||
|
missing_counts[field] += 1
|
||||||
|
for field in raw_derived or []:
|
||||||
|
derived_counts[field] += 1
|
||||||
price = scope_values.get("daily.close") or scope_values.get("close")
|
price = scope_values.get("daily.close") or scope_values.get("close")
|
||||||
if price is None:
|
if price is None:
|
||||||
continue
|
continue
|
||||||
@ -756,6 +811,15 @@ class BacktestEngine:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if missing_counts or derived_counts:
|
||||||
|
result.data_gaps.append(
|
||||||
|
{
|
||||||
|
"trade_date": trade_date_str,
|
||||||
|
"missing_fields": dict(sorted(missing_counts.items())),
|
||||||
|
"derived_fields": dict(sorted(derived_counts.items())),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
market_value = 0.0
|
market_value = 0.0
|
||||||
unrealized_pnl = 0.0
|
unrealized_pnl = 0.0
|
||||||
for ts_code, qty in state.holdings.items():
|
for ts_code, qty in state.holdings.items():
|
||||||
@ -1129,6 +1193,19 @@ def _persist_backtest_results(cfg: BtConfig, result: BacktestResult) -> None:
|
|||||||
)
|
)
|
||||||
summary_payload["risk_breakdown"] = breakdown
|
summary_payload["risk_breakdown"] = breakdown
|
||||||
|
|
||||||
|
if getattr(result, "data_gaps", None):
|
||||||
|
missing_total: Dict[str, int] = defaultdict(int)
|
||||||
|
derived_total: Dict[str, int] = defaultdict(int)
|
||||||
|
for gap in result.data_gaps:
|
||||||
|
for field, count in (gap.get("missing_fields") or {}).items():
|
||||||
|
missing_total[field] += int(count)
|
||||||
|
for field, count in (gap.get("derived_fields") or {}).items():
|
||||||
|
derived_total[field] += int(count)
|
||||||
|
if missing_total:
|
||||||
|
summary_payload["missing_field_counts"] = dict(missing_total)
|
||||||
|
if derived_total:
|
||||||
|
summary_payload["derived_field_counts"] = dict(derived_total)
|
||||||
|
|
||||||
cfg_payload = {
|
cfg_payload = {
|
||||||
"id": cfg.id,
|
"id": cfg.id,
|
||||||
"name": cfg.name,
|
"name": cfg.name,
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import streamlit as st
|
|||||||
from app.agents.base import AgentContext
|
from app.agents.base import AgentContext
|
||||||
from app.agents.game import Decision
|
from app.agents.game import Decision
|
||||||
from app.agents.registry import default_agents
|
from app.agents.registry import default_agents
|
||||||
|
from app.agents.protocols import GameStructure
|
||||||
from app.backtest.decision_env import DecisionEnv, ParameterSpec
|
from app.backtest.decision_env import DecisionEnv, ParameterSpec
|
||||||
from app.backtest.optimizer import BanditConfig, EpsilonGreedyBandit
|
from app.backtest.optimizer import BanditConfig, EpsilonGreedyBandit
|
||||||
from app.rl import TORCH_AVAILABLE, DecisionEnvAdapter, PPOConfig, train_ppo
|
from app.rl import TORCH_AVAILABLE, DecisionEnvAdapter, PPOConfig, train_ppo
|
||||||
@ -67,6 +68,16 @@ def render_backtest_review() -> None:
|
|||||||
target = col_target.number_input("目标收益(例:0.035 表示 3.5%)", value=0.035, step=0.005, format="%.3f", key="bt_target")
|
target = col_target.number_input("目标收益(例:0.035 表示 3.5%)", value=0.035, step=0.005, format="%.3f", key="bt_target")
|
||||||
stop = col_stop.number_input("止损收益(例:-0.015 表示 -1.5%)", value=-0.015, step=0.005, format="%.3f", key="bt_stop")
|
stop = col_stop.number_input("止损收益(例:-0.015 表示 -1.5%)", value=-0.015, step=0.005, format="%.3f", key="bt_stop")
|
||||||
hold_days = col_hold.number_input("持有期(交易日)", value=10, step=1, key="bt_hold_days")
|
hold_days = col_hold.number_input("持有期(交易日)", value=10, step=1, key="bt_hold_days")
|
||||||
|
structure_options = [item.value for item in GameStructure]
|
||||||
|
selected_structure_values = st.multiselect(
|
||||||
|
"选择博弈框架",
|
||||||
|
structure_options,
|
||||||
|
default=structure_options,
|
||||||
|
key="bt_game_structures",
|
||||||
|
)
|
||||||
|
if not selected_structure_values:
|
||||||
|
selected_structure_values = [GameStructure.REPEATED.value]
|
||||||
|
selected_structures = [GameStructure(value) for value in selected_structure_values]
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"当前回测表单输入:start=%s end=%s universe_text=%s target=%.3f stop=%.3f hold_days=%s",
|
"当前回测表单输入:start=%s end=%s universe_text=%s target=%.3f stop=%.3f hold_days=%s",
|
||||||
start_date,
|
start_date,
|
||||||
@ -148,6 +159,7 @@ def render_backtest_review() -> None:
|
|||||||
"stop": stop,
|
"stop": stop,
|
||||||
"hold_days": int(hold_days),
|
"hold_days": int(hold_days),
|
||||||
},
|
},
|
||||||
|
game_structures=selected_structures,
|
||||||
)
|
)
|
||||||
result = run_backtest(backtest_cfg, decision_callback=_decision_callback)
|
result = run_backtest(backtest_cfg, decision_callback=_decision_callback)
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
@ -287,6 +299,12 @@ def render_backtest_review() -> None:
|
|||||||
"风险分布": json.dumps(summary.get("risk_breakdown"), ensure_ascii=False)
|
"风险分布": json.dumps(summary.get("risk_breakdown"), ensure_ascii=False)
|
||||||
if summary.get("risk_breakdown")
|
if summary.get("risk_breakdown")
|
||||||
else None,
|
else None,
|
||||||
|
"缺失字段": json.dumps(summary.get("missing_field_counts"), ensure_ascii=False)
|
||||||
|
if summary.get("missing_field_counts")
|
||||||
|
else None,
|
||||||
|
"派生字段": json.dumps(summary.get("derived_field_counts"), ensure_ascii=False)
|
||||||
|
if summary.get("derived_field_counts")
|
||||||
|
else None,
|
||||||
}
|
}
|
||||||
metrics_rows.append({k: v for k, v in record.items() if (k == "cfg_id" or k in selected_metrics)})
|
metrics_rows.append({k: v for k, v in record.items() if (k == "cfg_id" or k in selected_metrics)})
|
||||||
if metrics_rows:
|
if metrics_rows:
|
||||||
@ -658,6 +676,7 @@ def render_backtest_review() -> None:
|
|||||||
"hold_days": int(hold_days),
|
"hold_days": int(hold_days),
|
||||||
},
|
},
|
||||||
method=app_cfg.decision_method,
|
method=app_cfg.decision_method,
|
||||||
|
game_structures=selected_structures,
|
||||||
)
|
)
|
||||||
env = DecisionEnv(
|
env = DecisionEnv(
|
||||||
bt_config=bt_cfg_env,
|
bt_config=bt_cfg_env,
|
||||||
@ -934,6 +953,7 @@ def render_backtest_review() -> None:
|
|||||||
"hold_days": int(hold_days),
|
"hold_days": int(hold_days),
|
||||||
},
|
},
|
||||||
method=app_cfg.decision_method,
|
method=app_cfg.decision_method,
|
||||||
|
game_structures=selected_structures,
|
||||||
)
|
)
|
||||||
env = DecisionEnv(
|
env = DecisionEnv(
|
||||||
bt_config=bt_cfg_env,
|
bt_config=bt_cfg_env,
|
||||||
|
|||||||
@ -74,6 +74,12 @@ class _RefreshCoordinator:
|
|||||||
normalized = parsed_date.strftime("%Y%m%d")
|
normalized = parsed_date.strftime("%Y%m%d")
|
||||||
tables = self._collect_tables(fields)
|
tables = self._collect_tables(fields)
|
||||||
if tables and self.broker.check_data_availability(normalized, tables):
|
if tables and self.broker.check_data_availability(normalized, tables):
|
||||||
|
LOGGER.debug(
|
||||||
|
"触发近端数据刷新 trade_date=%s tables=%s",
|
||||||
|
normalized,
|
||||||
|
sorted(tables),
|
||||||
|
extra=LOG_EXTRA,
|
||||||
|
)
|
||||||
self.broker._trigger_background_refresh(normalized)
|
self.broker._trigger_background_refresh(normalized)
|
||||||
|
|
||||||
def ensure_for_series(self, end_date: str, table: str) -> None:
|
def ensure_for_series(self, end_date: str, table: str) -> None:
|
||||||
@ -82,6 +88,12 @@ class _RefreshCoordinator:
|
|||||||
return
|
return
|
||||||
normalized = parsed_date.strftime("%Y%m%d")
|
normalized = parsed_date.strftime("%Y%m%d")
|
||||||
if self.broker.check_data_availability(normalized, {table}):
|
if self.broker.check_data_availability(normalized, {table}):
|
||||||
|
LOGGER.debug(
|
||||||
|
"触发序列刷新 trade_date=%s table=%s",
|
||||||
|
normalized,
|
||||||
|
table,
|
||||||
|
extra=LOG_EXTRA,
|
||||||
|
)
|
||||||
self.broker._trigger_background_refresh(normalized)
|
self.broker._trigger_background_refresh(normalized)
|
||||||
|
|
||||||
def _collect_tables(self, fields: Iterable[str]) -> Set[str]:
|
def _collect_tables(self, fields: Iterable[str]) -> Set[str]:
|
||||||
|
|||||||
@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
### 3.3 代码改造计划(多轮博弈适配)
|
### 3.3 代码改造计划(多轮博弈适配)
|
||||||
1. 架构基线评估
|
1. 架构基线评估
|
||||||
- ⏳ 绘制代理/部门/回测调用图,补充日志字段(缺数告警、补数来源、议程标识)并形成诊断报告。
|
- ✅ 绘制代理/部门/回测调用图并补充日志字段(见 docs/architecture_call_graph.md)。
|
||||||
- ✅ 定义多轮博弈上下文结构(消息历史、信念状态、引用证据),输出数据类与通信协议草稿。
|
- ✅ 定义多轮博弈上下文结构(消息历史、信念状态、引用证据),输出数据类与通信协议草稿。
|
||||||
- ✅ 在 `app/agents/protocols.py` 基础上补充主持/执行状态管理,实现 `DialogueTrace` 与部门上下文的对接路径。
|
- ✅ 在 `app/agents/protocols.py` 基础上补充主持/执行状态管理,实现 `DialogueTrace` 与部门上下文的对接路径。
|
||||||
- ✅ 扩展 `Decision.rounds` 与 `RoundSummary` 采集策略,用于串联部门结论与多轮议程结果。
|
- ✅ 扩展 `Decision.rounds` 与 `RoundSummary` 采集策略,用于串联部门结论与多轮议程结果。
|
||||||
@ -67,15 +67,16 @@
|
|||||||
- ✅ 将风险建议与执行回合对齐,执行阶段识别 `risk_adjusted` 并记录原始动作。
|
- ✅ 将风险建议与执行回合对齐,执行阶段识别 `risk_adjusted` 并记录原始动作。
|
||||||
2. 数据与因子重构
|
2. 数据与因子重构
|
||||||
- ✅ 拆分 `DataBroker` 查询层(`BrokerQueryEngine`),补数逻辑独立于查询管道。
|
- ✅ 拆分 `DataBroker` 查询层(`BrokerQueryEngine`),补数逻辑独立于查询管道。
|
||||||
- ⏳ 按主题拆分因子模块,存储缺口/异常标签,改写 `load_market_data()` 为“缺失即说明”。
|
- ⏳ 按主题拆分因子模块,存储缺口/异常标签。
|
||||||
- ⏳ 维护博弈结构 → 数据 scope 映射,支持角色按结构加载差异化字段。
|
- ✅ `load_market_data()` 标注缺失字段并写入原始日志(`missing_fields`、`derived_fields`)。
|
||||||
|
- ✅ 维护博弈结构 → 数据 scope 映射(`app/agents/scopes.py`,`BacktestEngine.required_fields`)。
|
||||||
- ✅ 基于 `_RefreshCoordinator` 落地刷新队列与监控事件,拆分查询与补数路径。
|
- ✅ 基于 `_RefreshCoordinator` 落地刷新队列与监控事件,拆分查询与补数路径。
|
||||||
- ✅ 暴露 `DataBroker.register_refresh_callback()` 钩子,结合监控系统记录补数进度与失败重试。
|
- ✅ 暴露 `DataBroker.register_refresh_callback()` 钩子,结合监控系统记录补数进度与失败重试。
|
||||||
- ⏳ 统一补数回调日志格式(`LOG_EXTRA.stage=data_broker`),为后续指标预留数据源。
|
- ⏳ 统一补数回调日志格式(`LOG_EXTRA.stage=data_broker`),为后续指标预留数据源。
|
||||||
3. 多轮博弈框架
|
3. 多轮博弈框架
|
||||||
- ✅ 在 `app/agents/game.py` 抽象 `GameProtocol` 接口,扩展 `Decision` 记录多轮对话。
|
- ✅ 在 `app/agents/game.py` 抽象 `GameProtocol` 接口,扩展 `Decision` 记录多轮对话。
|
||||||
- ✅ 实现主持调度器驱动议程(信息→陈述→反驳→共识→执行),挂载风险复核机制。
|
- ✅ 实现主持调度器驱动议程(信息→陈述→反驳→共识→执行),挂载风险复核机制。
|
||||||
- ⏳ 引入信念修正规则与论证框架,支持证据引用和冲突裁决。
|
- ✅ 引入基础信念修正规则(`app/agents/beliefs.py`),汇总信念并记录冲突。
|
||||||
4. 执行与回测集成
|
4. 执行与回测集成
|
||||||
- ✅ 将回测循环改造成“每日多轮→执行摘要”,完成风控校验与冲突重议流程。
|
- ✅ 将回测循环改造成“每日多轮→执行摘要”,完成风控校验与冲突重议流程。
|
||||||
- ⏳ 擦合订单映射层,明确多轮结果对应目标仓位、执行节奏、异常回滚策略。
|
- ⏳ 擦合订单映射层,明确多轮结果对应目标仓位、执行节奏、异常回滚策略。
|
||||||
|
|||||||
24
docs/architecture_call_graph.dot
Normal file
24
docs/architecture_call_graph.dot
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
digraph LLMQuantCallGraph {
|
||||||
|
rankdir=LR;
|
||||||
|
node [shape=box, style=rounded];
|
||||||
|
|
||||||
|
BacktestEngine -> LoadMarketData;
|
||||||
|
LoadMarketData -> DataBrokerFetch;
|
||||||
|
DataBrokerFetch -> BrokerQuery [label="db_session"];
|
||||||
|
LoadMarketData -> FeatureAssembly;
|
||||||
|
|
||||||
|
BacktestEngine -> Decide;
|
||||||
|
Decide -> ProtocolHost;
|
||||||
|
ProtocolHost -> DepartmentRound;
|
||||||
|
ProtocolHost -> RiskReview;
|
||||||
|
ProtocolHost -> ExecutionSummary;
|
||||||
|
Decide -> BeliefRevision;
|
||||||
|
|
||||||
|
ExecutionSummary -> ApplyPortfolio;
|
||||||
|
ApplyPortfolio -> RiskEvents;
|
||||||
|
ApplyPortfolio -> Alerts;
|
||||||
|
ApplyPortfolio -> PersistResults;
|
||||||
|
|
||||||
|
PersistResults -> Reports;
|
||||||
|
PersistResults -> UI;
|
||||||
|
}
|
||||||
43
docs/architecture_call_graph.md
Normal file
43
docs/architecture_call_graph.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# 多轮博弈决策调用示意
|
||||||
|
|
||||||
|
本节概述 `llm_quant` 中多轮博弈执行链路,便于定位关键日志与扩展点。
|
||||||
|
|
||||||
|
```
|
||||||
|
BacktestEngine.simulate_day
|
||||||
|
└─ load_market_data
|
||||||
|
├─ DataBroker.fetch_latest
|
||||||
|
│ ├─ BrokerQueryEngine.fetch_latest
|
||||||
|
│ └─ 缺失字段 → derived_fields/missing_fields (写入 raw/missing_fields)
|
||||||
|
├─ DataBroker.fetch_series (同上)
|
||||||
|
└─ assemble feature_map (features / market_snapshot / raw)
|
||||||
|
└─ for each symbol → decide (agents.game)
|
||||||
|
├─ compute_utilities / feasible_actions
|
||||||
|
├─ DepartmentManager.evaluate (LLM 部门,可带回 risk/rationale)
|
||||||
|
├─ ProtocolHost (game protocols)
|
||||||
|
│ ├─ start_round("department_consensus")
|
||||||
|
│ ├─ risk_review (当 conflict / risk assessment 触发)
|
||||||
|
│ └─ execution_summary (记录 execution_status)
|
||||||
|
├─ revise_beliefs (beliefs.py) → consensus/conflict
|
||||||
|
└─ Decision
|
||||||
|
├─ rounds (RoundSummary 日志)
|
||||||
|
├─ risk_assessment (status/reason/recommended_action)
|
||||||
|
├─ belief_updates / belief_revision (供监控/重播)
|
||||||
|
└─ department_votes / utilities
|
||||||
|
└─ _apply_portfolio_updates
|
||||||
|
├─ 使用 Decision.risk_assessment 调节执行
|
||||||
|
├─ 执行失败/阻断 → risk_events & alerts.backtest_risk
|
||||||
|
└─ executed_trades / nav_series / risk_events → bt_* 表
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键日志
|
||||||
|
- `LOG_EXTRA = {"stage": "backtest"}`:缺失字段、派生字段、执行阻断。
|
||||||
|
- `LOG_EXTRA = {"stage": "data_broker"}`:自动补数触发、查询失败回退。
|
||||||
|
|
||||||
|
## 拉通数据
|
||||||
|
- `app/agents/scopes.py` 维护结构 → 字段映射。
|
||||||
|
- `Decision.raw` 中 `missing_fields/derived_fields` 可用于缺口诊断。
|
||||||
|
|
||||||
|
## 后续建议
|
||||||
|
1. 将 `belief_revision` 与 `risk_events` 接入监控告警。
|
||||||
|
2. 结合 `missing_fields` 统计生成数据质量简报。
|
||||||
|
3. 通过自动化脚本渲染上述流程图/时序图。
|
||||||
44
scripts/render_architecture_diagram.py
Normal file
44
scripts/render_architecture_diagram.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""Render architecture call graph for multi-agent decision flow."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DOT_TEMPLATE = """
|
||||||
|
digraph LLMQuantCallGraph {
|
||||||
|
rankdir=LR;
|
||||||
|
node [shape=box, style=rounded];
|
||||||
|
|
||||||
|
BacktestEngine -> LoadMarketData;
|
||||||
|
LoadMarketData -> DataBrokerFetch;
|
||||||
|
DataBrokerFetch -> BrokerQuery [label="db_session"];
|
||||||
|
LoadMarketData -> FeatureAssembly;
|
||||||
|
|
||||||
|
BacktestEngine -> Decide;
|
||||||
|
Decide -> ProtocolHost;
|
||||||
|
ProtocolHost -> DepartmentRound;
|
||||||
|
ProtocolHost -> RiskReview;
|
||||||
|
ProtocolHost -> ExecutionSummary;
|
||||||
|
Decide -> BeliefRevision;
|
||||||
|
|
||||||
|
ExecutionSummary -> ApplyPortfolio;
|
||||||
|
ApplyPortfolio -> RiskEvents;
|
||||||
|
ApplyPortfolio -> Alerts;
|
||||||
|
ApplyPortfolio -> PersistResults;
|
||||||
|
|
||||||
|
PersistResults -> Reports;
|
||||||
|
PersistResults -> UI;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def render(output: Path) -> None:
|
||||||
|
output.write_text(DOT_TEMPLATE.strip() + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
out_file = Path("docs/architecture_call_graph.dot")
|
||||||
|
render(out_file)
|
||||||
|
print(f"dot file written: {out_file}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user