Initial commit of integrated agent system
This commit is contained in:
362
backend/utils/settlement.py
Normal file
362
backend/utils/settlement.py
Normal file
@@ -0,0 +1,362 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Settlement Coordinator
|
||||
Unified daily settlement logic for agent portfolio, baselines, and analyst tracking
|
||||
"""
|
||||
# flake8: noqa: E501
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from backend.services.storage import StorageService
|
||||
from backend.utils.analyst_tracker import (
|
||||
AnalystPerformanceTracker,
|
||||
update_leaderboard_with_evaluations,
|
||||
)
|
||||
from backend.utils.baselines import (
|
||||
BaselineCalculator,
|
||||
calculate_momentum_scores,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettlementCoordinator:
|
||||
"""
|
||||
Coordinates daily settlement after market close
|
||||
|
||||
Responsibilities:
|
||||
1. Calculate agent portfolio P&L
|
||||
2. Update baseline portfolios (equal-weight, market-cap, momentum)
|
||||
3. Evaluate analyst predictions and update leaderboard
|
||||
4. Update summary.json with all portfolio values
|
||||
5. Persist state to storage
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
storage: "StorageService",
|
||||
initial_capital: float = 100000.0,
|
||||
):
|
||||
self.storage = storage
|
||||
self.initial_capital = initial_capital
|
||||
self.baseline_calculator = BaselineCalculator(initial_capital)
|
||||
self.analyst_tracker = AnalystPerformanceTracker()
|
||||
|
||||
self.price_history: Dict[str, List[tuple]] = {}
|
||||
|
||||
# Load persisted state from storage
|
||||
self._load_persisted_state()
|
||||
|
||||
def _load_persisted_state(self):
|
||||
"""
|
||||
Load persisted baseline and price history state from storage
|
||||
|
||||
This restores the baseline calculator state so that backtest/live mode
|
||||
can resume from where it left off.
|
||||
"""
|
||||
internal_state = self.storage.load_internal_state()
|
||||
|
||||
# Load baseline calculator state
|
||||
baseline_state = {
|
||||
"baseline_state": internal_state.get("baseline_state", {}),
|
||||
"baseline_vw_state": internal_state.get("baseline_vw_state", {}),
|
||||
"momentum_state": internal_state.get("momentum_state", {}),
|
||||
}
|
||||
self.baseline_calculator.load_state(baseline_state)
|
||||
|
||||
# Load price history for momentum calculation
|
||||
saved_price_history = internal_state.get("price_history", {})
|
||||
if saved_price_history:
|
||||
# Convert saved format back to list of tuples
|
||||
for ticker, history in saved_price_history.items():
|
||||
converted_history = []
|
||||
for entry in history:
|
||||
if isinstance(entry, dict):
|
||||
converted_history.append(
|
||||
(entry["date"], entry["price"]),
|
||||
)
|
||||
elif isinstance(entry, (list, tuple)) and len(entry) >= 2:
|
||||
converted_history.append((entry[0], entry[1]))
|
||||
else:
|
||||
continue
|
||||
self.price_history[ticker] = converted_history
|
||||
logger.info(
|
||||
f"Restored price history for {len(self.price_history)} tickers",
|
||||
)
|
||||
|
||||
def _save_persisted_state(self):
|
||||
"""
|
||||
Save baseline and price history state to storage
|
||||
|
||||
This persists the baseline calculator state so that backtest/live mode
|
||||
can resume from where it left off after restart.
|
||||
"""
|
||||
internal_state = self.storage.load_internal_state()
|
||||
|
||||
# Export baseline calculator state
|
||||
baseline_state = self.baseline_calculator.export_state()
|
||||
internal_state["baseline_state"] = baseline_state["baseline_state"]
|
||||
internal_state["baseline_vw_state"] = baseline_state[
|
||||
"baseline_vw_state"
|
||||
]
|
||||
internal_state["momentum_state"] = baseline_state["momentum_state"]
|
||||
|
||||
# Save price history (convert tuples to dicts for JSON serialization)
|
||||
price_history_serializable = {}
|
||||
for ticker, history in self.price_history.items():
|
||||
price_history_serializable[ticker] = [
|
||||
{"date": date, "price": price} for date, price in history
|
||||
]
|
||||
internal_state["price_history"] = price_history_serializable
|
||||
|
||||
self.storage.save_internal_state(internal_state)
|
||||
logger.info("Persisted baseline calculator and price history state")
|
||||
|
||||
def record_analyst_predictions(
|
||||
self,
|
||||
final_predictions: List[Dict[str, Any]],
|
||||
):
|
||||
"""
|
||||
Record structured analyst predictions before market close
|
||||
|
||||
Args:
|
||||
final_predictions: Structured prediction results from analysts
|
||||
Format: [
|
||||
{
|
||||
'agent': 'analyst_name',
|
||||
'predictions': [
|
||||
{'ticker': 'AAPL', 'direction': 'up', 'confidence': 0.75},
|
||||
...
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
tickers: List of tickers being analyzed
|
||||
"""
|
||||
self.analyst_tracker.record_analyst_predictions(final_predictions)
|
||||
|
||||
def update_price_history(
|
||||
self,
|
||||
date: str,
|
||||
prices: Dict[str, float],
|
||||
):
|
||||
"""
|
||||
Update price history for momentum calculation
|
||||
|
||||
Args:
|
||||
date: Trading date (YYYY-MM-DD)
|
||||
prices: Current prices for each ticker
|
||||
"""
|
||||
for ticker, price in prices.items():
|
||||
if ticker not in self.price_history:
|
||||
self.price_history[ticker] = []
|
||||
self.price_history[ticker].append((date, price))
|
||||
|
||||
self.price_history[ticker] = self.price_history[ticker][-60:]
|
||||
|
||||
def run_daily_settlement(
|
||||
self,
|
||||
date: str,
|
||||
tickers: List[str],
|
||||
open_prices: Optional[Dict[str, float]],
|
||||
close_prices: Dict[str, float],
|
||||
market_caps: Dict[str, float],
|
||||
agent_portfolio: Dict[str, Any],
|
||||
analyst_results: List[Dict[str, Any]], # pylint: disable=W0613
|
||||
pm_decisions: Optional[Dict[str, Dict]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run complete daily settlement
|
||||
|
||||
Args:
|
||||
date: Trading date (YYYY-MM-DD)
|
||||
tickers: List of tickers
|
||||
open_prices: Opening prices
|
||||
close_prices: Closing prices
|
||||
market_caps: Market caps for each ticker
|
||||
agent_portfolio: Current agent portfolio state
|
||||
analyst_results: Analyst analysis results
|
||||
pm_decisions: PM's trading decisions
|
||||
|
||||
Returns:
|
||||
Settlement results including all portfolio values and evaluations
|
||||
"""
|
||||
logger.info(f"Running daily settlement for {date}")
|
||||
|
||||
self.update_price_history(date, close_prices)
|
||||
|
||||
momentum_scores = calculate_momentum_scores(
|
||||
tickers,
|
||||
self.price_history,
|
||||
lookback_days=20,
|
||||
)
|
||||
|
||||
rebalance_momentum = self._should_rebalance_momentum(date)
|
||||
|
||||
baseline_values = self.baseline_calculator.get_all_baseline_values(
|
||||
tickers=tickers,
|
||||
open_prices=open_prices if open_prices else close_prices,
|
||||
close_prices=close_prices,
|
||||
market_caps=market_caps,
|
||||
momentum_scores=momentum_scores,
|
||||
date=date,
|
||||
rebalance_momentum=rebalance_momentum,
|
||||
)
|
||||
|
||||
logger.info(f"Baseline values calculated: {baseline_values}")
|
||||
|
||||
agent_value = self.storage.calculate_portfolio_value(
|
||||
agent_portfolio,
|
||||
close_prices,
|
||||
)
|
||||
|
||||
analyst_evaluations = self.analyst_tracker.evaluate_predictions(
|
||||
open_prices,
|
||||
close_prices,
|
||||
date,
|
||||
)
|
||||
|
||||
pm_evaluations = {}
|
||||
if pm_decisions:
|
||||
pm_evaluations = self.analyst_tracker.evaluate_pm_decisions(
|
||||
pm_decisions,
|
||||
open_prices,
|
||||
close_prices,
|
||||
date,
|
||||
)
|
||||
|
||||
all_evaluations = {**analyst_evaluations, **pm_evaluations}
|
||||
|
||||
leaderboard = self.storage.load_export_file("leaderboard") or []
|
||||
updated_leaderboard = update_leaderboard_with_evaluations(
|
||||
leaderboard,
|
||||
all_evaluations,
|
||||
)
|
||||
self.storage.save_export_file("leaderboard", updated_leaderboard)
|
||||
|
||||
self._update_summary_with_baselines(
|
||||
date,
|
||||
agent_value,
|
||||
baseline_values,
|
||||
)
|
||||
|
||||
self.analyst_tracker.clear_daily_predictions()
|
||||
|
||||
# Persist baseline calculator and price history state
|
||||
self._save_persisted_state()
|
||||
|
||||
return {
|
||||
"date": date,
|
||||
"agent_portfolio_value": agent_value,
|
||||
"baseline_values": baseline_values,
|
||||
"analyst_evaluations": analyst_evaluations,
|
||||
"baselines_updated": True,
|
||||
"leaderboard_updated": True,
|
||||
}
|
||||
|
||||
def _should_rebalance_momentum(self, date: str) -> bool:
|
||||
"""
|
||||
Check if momentum portfolio should rebalance
|
||||
|
||||
Returns True if it's a new month
|
||||
"""
|
||||
last_rebalance = self.baseline_calculator.momentum_last_rebalance_date
|
||||
if last_rebalance is None:
|
||||
return True
|
||||
|
||||
last_date = datetime.strptime(last_rebalance, "%Y-%m-%d")
|
||||
current_date = datetime.strptime(date, "%Y-%m-%d")
|
||||
|
||||
return (current_date.year, current_date.month) != (
|
||||
last_date.year,
|
||||
last_date.month,
|
||||
)
|
||||
|
||||
def _update_summary_with_baselines(
|
||||
self,
|
||||
date: str,
|
||||
agent_value: float,
|
||||
baseline_values: Dict[str, float],
|
||||
):
|
||||
"""
|
||||
Update summary.json with agent and baseline portfolio values
|
||||
|
||||
NOTE: History updates are now handled centrally by storage.update_dashboard_after_cycle()
|
||||
to ensure all histories (equity, baseline, baseline_vw, momentum) stay synchronized.
|
||||
baseline_values are returned in run_daily_settlement() and passed to storage.
|
||||
|
||||
Args:
|
||||
date: Trading date (used for backtest-compatible timestamps)
|
||||
agent_value: Agent portfolio value
|
||||
baseline_values: Baseline portfolio values
|
||||
"""
|
||||
# History updates are now handled by storage.update_dashboard_after_cycle()
|
||||
# which receives baseline_values from settlement_result and updates all histories together.
|
||||
# This ensures equity and baseline data points are always synchronized.
|
||||
|
||||
def update_intraday_values(
|
||||
self,
|
||||
tickers: List[str],
|
||||
current_prices: Dict[str, float],
|
||||
market_caps: Dict[str, float],
|
||||
agent_portfolio: Dict[str, Any],
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Update portfolio values with current prices (for live mode intraday updates)
|
||||
|
||||
Args:
|
||||
tickers: List of tickers
|
||||
current_prices: Current prices
|
||||
market_caps: Market caps
|
||||
agent_portfolio: Current agent portfolio
|
||||
|
||||
Returns:
|
||||
Dict with current portfolio values
|
||||
"""
|
||||
agent_value = self.storage.calculate_portfolio_value(
|
||||
agent_portfolio,
|
||||
current_prices,
|
||||
)
|
||||
|
||||
equal_weight = self.baseline_calculator.calculate_equal_weight_value(
|
||||
tickers,
|
||||
current_prices,
|
||||
current_prices,
|
||||
)
|
||||
market_cap = (
|
||||
self.baseline_calculator.calculate_market_cap_weighted_value(
|
||||
tickers,
|
||||
current_prices,
|
||||
current_prices,
|
||||
market_caps,
|
||||
)
|
||||
)
|
||||
|
||||
momentum_scores = calculate_momentum_scores(
|
||||
tickers,
|
||||
self.price_history,
|
||||
lookback_days=20,
|
||||
)
|
||||
|
||||
last_date = (
|
||||
list(self.price_history.values())[0][-1][0]
|
||||
if self.price_history
|
||||
else ""
|
||||
)
|
||||
|
||||
momentum = self.baseline_calculator.calculate_momentum_value(
|
||||
tickers,
|
||||
current_prices,
|
||||
current_prices,
|
||||
momentum_scores,
|
||||
date=last_date,
|
||||
rebalance=False,
|
||||
)
|
||||
|
||||
return {
|
||||
"agent": agent_value,
|
||||
"equal_weight": equal_weight,
|
||||
"market_cap_weighted": market_cap,
|
||||
"momentum": momentum,
|
||||
}
|
||||
Reference in New Issue
Block a user