File size: 6,812 Bytes
9b88b42
 
 
 
 
 
6752363
9b88b42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6752363
 
9b88b42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6752363
9b88b42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6752363
9b88b42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6752363
9b88b42
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""Portfolio data models.

This module defines Pydantic models for portfolio data structures, holdings,
and analysis results.
"""

from datetime import datetime, timezone
from decimal import Decimal
from typing import Optional, List, Dict, Any
from enum import Enum

from pydantic import BaseModel, Field, field_validator, ConfigDict


class RiskTolerance(str, Enum):
    """Risk tolerance levels."""

    CONSERVATIVE = "conservative"
    MODERATE = "moderate"
    AGGRESSIVE = "aggressive"


class AssetType(str, Enum):
    """Asset type classification."""

    STOCK = "stock"
    ETF = "etf"
    CRYPTO = "crypto"
    BOND = "bond"
    CASH = "cash"
    OTHER = "other"


class Holding(BaseModel):
    """Individual portfolio holding."""

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )

    ticker: str = Field(..., description="Stock ticker symbol")
    quantity: Decimal = Field(..., gt=0, description="Number of shares/units")
    cost_basis: Optional[Decimal] = Field(
        None, ge=0, description="Purchase price per share"
    )
    asset_type: AssetType = Field(
        default=AssetType.STOCK, description="Type of asset"
    )
    current_price: Optional[Decimal] = Field(
        None, ge=0, description="Current market price"
    )
    current_value: Optional[Decimal] = Field(
        None, ge=0, description="Current total value"
    )

    @field_validator("ticker")
    @classmethod
    def validate_ticker(cls, v: str) -> str:
        """Validate ticker format."""
        v = v.strip().upper()
        if not v:
            raise ValueError("Ticker cannot be empty")
        if len(v) > 10:
            raise ValueError("Ticker too long (max 10 characters)")
        return v


class Portfolio(BaseModel):
    """Portfolio containing multiple holdings."""

    model_config = ConfigDict(
        validate_assignment=True,
    )

    portfolio_id: Optional[str] = Field(None, description="Unique portfolio ID")
    user_id: Optional[str] = Field(None, description="Owner user ID")
    name: str = Field(..., min_length=1, max_length=200, description="Portfolio name")
    description: Optional[str] = Field(None, max_length=1000)
    holdings: List[Holding] = Field(default_factory=list, min_length=1)
    risk_tolerance: RiskTolerance = Field(default=RiskTolerance.MODERATE)
    total_value: Optional[Decimal] = Field(None, ge=0)
    created_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc))
    updated_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc))

    @field_validator("holdings")
    @classmethod
    def validate_holdings(cls, v: List[Holding]) -> List[Holding]:
        """Validate holdings list."""
        if not v:
            raise ValueError("Portfolio must have at least one holding")
        if len(v) > 100:
            raise ValueError("Portfolio cannot have more than 100 holdings")
        return v


class MarketData(BaseModel):
    """Market data for a single security."""

    ticker: str
    price: Decimal = Field(..., ge=0)
    previous_close: Optional[Decimal] = Field(None, ge=0)
    open_price: Optional[Decimal] = Field(None, ge=0)
    high: Optional[Decimal] = Field(None, ge=0)
    low: Optional[Decimal] = Field(None, ge=0)
    volume: Optional[int] = Field(None, ge=0)
    market_cap: Optional[Decimal] = Field(None, ge=0)
    pe_ratio: Optional[Decimal] = None
    dividend_yield: Optional[Decimal] = Field(None, ge=0, le=1)
    fifty_two_week_high: Optional[Decimal] = Field(None, ge=0)
    fifty_two_week_low: Optional[Decimal] = Field(None, ge=0)
    timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class HistoricalData(BaseModel):
    """Historical price data."""

    ticker: str
    dates: List[datetime]
    prices: List[Decimal]
    volumes: Optional[List[int]] = None
    returns: Optional[List[Decimal]] = None


class OptimisationResult(BaseModel):
    """Portfolio optimisation result."""

    method: str = Field(..., description="Optimisation method used")
    weights: Dict[str, Decimal] = Field(..., description="Ticker to weight mapping")
    expected_return: Decimal = Field(..., description="Expected annual return")
    volatility: Decimal = Field(..., ge=0, description="Expected volatility")
    sharpe_ratio: Decimal = Field(..., description="Sharpe ratio")
    metadata: Optional[Dict[str, Any]] = Field(default_factory=dict)


class RiskMetrics(BaseModel):
    """Portfolio risk metrics."""

    volatility: Decimal = Field(..., ge=0, description="Annualised volatility")
    sharpe_ratio: Decimal = Field(..., description="Sharpe ratio")
    sortino_ratio: Optional[Decimal] = Field(None, description="Sortino ratio")
    max_drawdown: Decimal = Field(..., le=0, description="Maximum drawdown")
    var_95: Decimal = Field(..., le=0, description="Value at Risk (95%)")
    var_99: Decimal = Field(..., le=0, description="Value at Risk (99%)")
    cvar_95: Decimal = Field(..., le=0, description="Conditional VaR (95%)")
    cvar_99: Decimal = Field(..., le=0, description="Conditional VaR (99%)")
    beta: Optional[Decimal] = Field(None, description="Beta vs benchmark")
    alpha: Optional[Decimal] = Field(None, description="Alpha vs benchmark")


class PortfolioAnalysis(BaseModel):
    """Complete portfolio analysis result."""

    portfolio_id: str
    timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

    # Current state
    total_value: Decimal = Field(..., ge=0)
    holdings_count: int = Field(..., ge=1)

    # Market data
    market_data: Dict[str, MarketData] = Field(default_factory=dict)

    # Risk metrics
    risk_metrics: RiskMetrics

    # Optimisation results
    optimisation_hrp: Optional[OptimisationResult] = None
    optimisation_black_litterman: Optional[OptimisationResult] = None
    optimisation_mean_variance: Optional[OptimisationResult] = None

    # AI-generated insights
    summary: str = Field(..., min_length=10)
    recommendations: List[str] = Field(default_factory=list)
    risk_assessment: str
    health_score: int = Field(..., ge=0, le=100)

    # Agent reasoning
    reasoning_steps: Optional[List[str]] = Field(default_factory=list)
    mcp_calls: Optional[List[Dict[str, Any]]] = Field(default_factory=list)

    # Metadata
    execution_time_ms: Optional[int] = Field(None, ge=0)
    model_version: Optional[str] = None


class MCPProvenance(BaseModel):
    """Data provenance tracking for MCP calls."""

    source: str = Field(..., description="MCP source identifier")
    mcps_used: List[str] = Field(..., description="List of MCP servers called")
    fetch_timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    cache_hit: bool = Field(default=False)