Files
invest-copilot/docs/03-domain-model.md
T

375 lines
15 KiB
Markdown
Raw Normal View History

2026-05-30 11:28:59 -04:00
# Invest Copilot — Domain Model (DDD)
## Bounded Contexts
```
┌─────────────────────────────────────────────────────────────────┐
│ INVEST COPILOT SYSTEM │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ RESEARCH │ │ PORTFOLIO │ │ ALERTEVENTS │ │
│ │ CONTEXT │ │ CONTEXT │ │ CONTEXT │ │
│ │ │ │ │ │ │ │
│ │ Stock │ │ Watchlist │ │ Watchlist │ │
│ │ Profile │ │ Strategy │ │ Agent │ │
│ │ Peer Group │ │ Screener │ │ Alert Engine │ │
│ │ SEC Filing │ │ Backtest │ │ Notification │ │
│ │ Sentiment │ │ Rotation │ │ Monitoring │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ │ ┌──────────────┴──────────────────────┐ │
│ │ │ SHARED KERNEL │ │
│ │ │ MarketData, Ticker, Sector, │ │
│ │ │ PriceEvent, SectorRotation │ │
│ │ └───────────────────────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ APPLICATION LAYER │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ Stock │ │ Watch │ │ Strategy│ │ Alert │ │
│ │ Service │ │ list │ │ Service │ │ Service │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────────┘ │
├──────────────────────────────────────────────────────────────────┤
│ DOMAIN LAYER │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ Stock │ │ Watch- │ │ Strategy │ │ Alert │ │
│ │ Entity │ │ list │ │ Entity │ │ Entity │ │
│ │ Aggr │ │ Aggr │ │ Aggr │ │ Aggr │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────────┘ │
├──────────────────────────────────────────────────────────────────┤
│ INFRASTRUCTURE LAYER │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ Price │ │ SEC │ │ Strategy│ │ SSE │ │
│ │ Rep │ │ Rep │ │ Rep │ │ Push │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
## Bounded Context 1: RESEARCH
**Responsibility**: Provide comprehensive stock information and analysis.
### Entities & Value Objects
```
Stock (Aggregate Root)
├── ticker: Ticker
├── name: string
├── sector: Sector (Value Object)
├── industry: string
├── marketCap: Money (Value Object)
├── exchange: string
└── profile: CompanyProfile (Value Object)
CompanyProfile (Value Object)
├── description: string
├── website: string
├── ceo: string
├── employees: int
├── peRatio: decimal
├── eps: decimal
├── dividendYield: decimal
└── beta: decimal
Sector (Value Object)
├── code: string // e.g., "XLK"
├── name: string // e.g., "Technology"
├── gicsCode: string // e.g., "45"
└── peers: List<Ticker>
PeerGroup (Aggregate Root)
├── ticker: Ticker
├── peers: List<Peer>
└── similarityScores: Map<Ticker, float>
Peer (Value Object)
├── ticker: Ticker
├── name: string
├── similarityScore: float
└── relativeStrength: decimal
SecFiling (Entity within Stock aggregate)
├── formType: FormType (enum: 10K, 10Q, 8K, 4, 13F, 13D, 13G)
├── filingDate: Date
├── reportDate: Date
├── accessionNumber: string
├── url: string
├── contentSummary: string
├── sentimentScore: decimal
└── tags: List<string>
InsiderTrade (Entity within Stock aggregate)
├── insiderName: string
├── insiderTitle: string
├── transactionDate: Date
├── transactionType: TransactionType (enum: BUY, SELL, GIFT, IN_EX)
├── shares: int
├── pricePerShare: Money
├── totalValue: Money
└── sharesOwnedAfter: int
SentimentSignal (Value Object)
├── source: string // "finnhub", "reddit", "gdelt"
├── score: decimal // -1.0 to +1.0
├── confidence: float
├── timestamp: DateTime
└── context: string
```
### Domain Events
- `StockProfileUpdated` — Stock profile refreshed from data source
- `SecFilingReceived` — New SEC filing detected
- `InsiderTradeDetected` — New insider trade filed
- `SentimentShifted` — Sentiment score changed significantly
### Aggregates
- **Stock** is the root aggregate. All research data (filings, insider trades, sentiment, peer info) are either entities within this aggregate or related via repository.
## Bounded Context 2: PORTFOLIO (WATCHLIST + STRATEGY)
**Responsibility**: Manage watchlists, strategies, screeners, and sector rotation.
### Entities & Value Objects
```
Watchlist (Aggregate Root)
├── id: UUID
├── name: string
├── owner: UserId
├── items: List<WatchlistItem>
├── strategies: List<StrategyReference>
└── created/updated timestamps
WatchlistItem (Entity)
├── ticker: Ticker
├── type: AssetType (enum: STOCK, ETF, INDEX)
├── addedAt: DateTime
├── customNotes: string
└── priceAtAddition: Money
Strategy (Aggregate Root)
├── id: UUID
├── name: string
├── description: string
├── type: StrategyType (enum: TECHNICAL, FUNDAMENTAL, HYBRID)
├── conditions: StrategyConditions (Value Object)
├── backtestResults: BacktestResult (Value Object)
├── createdBy: UserId
└── created/updated timestamps
StrategyConditions (Value Object)
├── rules: List<StrategyRule>
├── logic: LogicOperator (AND / OR)
└── timeframe: string // e.g., "1D", "1W", "1M"
StrategyRule (Value Object)
├── indicator: string // "rsi", "sma", "macd", "pe_ratio", etc.
├── operator: Operator (enum: LT, GT, LTE, GTE, EQ, NEQ, CROSSOVER, CROSSBELOW)
├── value: decimal
├── period: int // optional, for indicators like SMA
├── source: string // optional, for SMA: "close", "volume"
└── description: string
BacktestResult (Value Object)
├── startDate: Date
├── endDate: Date
├── totalReturn: decimal
├── annualizedReturn: decimal
├── maxDrawdown: decimal
├── sharpeRatio: decimal
├── winRate: float
├── totalTrades: int
├── avgHoldTime: string
└── equityCurve: List<Decimal>
Screener (Aggregate Root)
├── id: UUID
├── name: string
├── description: string
├── conditions: ScreenerConditions (Value Object)
├── lastRunAt: DateTime
├── createdBy: UserId
└── results: List<ScreenerResult> (stored in screener_results table)
ScreenerConditions (Value Object)
├── filters: List<ScreenerFilter>
├── sortBy: string
├── sortOrder: string // "asc" / "desc"
└── limit: int
ScreenerFilter (Value Object)
├── field: string // "market_cap", "pe_ratio", "rsi", etc.
├── operator: Operator
├── value: decimal
└── description: string
SectorRotation (Entity within Watchlist context)
├── date: Date
├── sector: Sector
├── rankNow: int
├── rankPrevious: int
├── rankChange: int
├── momentum20d: decimal
├── momentum50d: decimal
├── momentum200d: decimal
├── relativeStrength: decimal
├── signal: RotationSignal (enum: IN, OUT, STABLE, ACCELERATING)
├── macroContext: jsonb
└── analysisSummary: string
```
### Domain Events
- `WatchlistCreated` — New watchlist created
- `WatchlistItemAdded` — Ticker added to watchlist
- `WatchlistItemRemoved` — Ticker removed from watchlist
- `StrategyCreated` — New strategy defined
- `StrategyTriggered` — Strategy condition met for a ticker
- `SectorRotationDetected` — Sector rotation event
- `ScreenerResultsGenerated` — Screener completed run
### Aggregates
- **Watchlist** is the root. Items and strategies are entities within it.
- **Strategy** is independent (can be shared across watchlists).
- **Screener** is independent per user.
- **SectorRotation** is stored as events, queried for history.
## Bounded Context 3: ALERTEVENTS
**Responsibility**: Monitor watchlists, evaluate triggers, dispatch notifications.
### Entities & Value Objects
```
Alert (Aggregate Root)
├── id: UUID
├── watchlist: Watchlist (reference)
├── strategy: Strategy? (nullable, some alerts are non-strategy)
├── ticker: Ticker
├── type: AlertType (enum: STRATEGY_TRIGGER, SEC_FILING, SENTIMENT, ROTATION, PRICE)
├── triggerType: string
├── message: string
├── severity: Severity (enum: INFO, WARNING, CRITICAL)
├── status: AlertStatus (enum: ACTIVE, RESOLVED, DISMISSED)
├── triggeredAt: DateTime
├── resolvedAt: DateTime?
└── metadata: jsonb
AlertTrigger (Value Object)
├── source: string // strategy name, filing type, etc.
├── condition: string // what triggered
├── currentValue: decimal
├── thresholdValue: decimal
└── timestamp: DateTime
Notification (Value Object)
├── channel: NotificationChannel (enum: SSE, EMAIL, PUSH)
├── recipient: UserId
├── alertId: UUID
├── sentAt: DateTime
├── delivered: boolean
└── error: string?
```
### Domain Events
- `AlertTriggered` — Alert condition met
- `AlertResolved` — Alert condition no longer applies
- `AlertDismissed` — User dismissed alert
- `NotificationSent` — Alert dispatched via channel
- `NotificationFailed` — Delivery failed
### Aggregates
- **Alert** is the root aggregate.
- Notifications are derived from alerts (not part of the same aggregate).
## Shared Kernel
These concepts are shared across bounded contexts with consistent definitions:
```
Ticker (Value Object)
├── symbol: string // e.g., "AAPL"
├── exchange: string // e.g., "NASDAQ"
└── isPrimary: boolean // for tickers with multiple listings
Sector (Value Object)
├── code: string // e.g., "XLK"
├── name: string // e.g., "Technology"
└── gicsCode: string // e.g., "45"
PriceEvent (Value Object)
├── date: DateTime
├── open: Money
├── high: Money
├── low: Money
├── close: Money
└── volume: long
Money (Value Object)
├── amount: decimal
├── currency: string // ISO 4217
UserId (Value Object)
├── id: UUID
└── email: string
```
## Anti-Corruption Layer
**External APIs → Domain Models**:
- Massive/Polygon API → PriceEvent (no external IDs leak in)
- SEC EDGAR → SecFiling (parse raw XML/JSON, produce clean domain object)
- Finnhub → SentimentSignal, InsiderTrade, NewsEvent
- Reddit API → SentimentSignal (from subreddit analysis)
- FRED → MacroeconomicData (for sector rotation context)
**Pattern**: Every external data source has a dedicated adapter that translates API responses into our domain models. No external schema leaks into the domain layer.
## Repository Interfaces (Domain Layer)
```typescript
// Domain layer defines interfaces, infrastructure implements them
interface IStockRepository {
findByTicker(ticker: Ticker): Promise<Stock | null>
findByTickers(tickers: Ticker[]): Promise<Stock[]>
save(stock: Stock): Promise<void>
getSecFilings(ticker: Ticker, limit?: number): Promise<SecFiling[]>
getInsiderTrades(ticker: Ticker, limit?: number): Promise<InsiderTrade[]>
getPeerGroup(ticker: Ticker): Promise<PeerGroup>
}
interface IPriceRepository {
getHistory(ticker: Ticker, from: Date, to: Date, interval: string): Promise<PriceEvent[]>
getLatest(ticker: Ticker): Promise<PriceEvent>
saveBatch(prices: PriceEvent[]): Promise<void>
getMomentum(ticker: Ticker, period: number): Promise<decimal>
}
interface IWatchlistRepository {
findByUser(userId: UserId): Promise<Watchlist[]>
findById(id: UUID): Promise<Watchlist | null>
save(watchlist: Watchlist): Promise<void>
addItem(watchlistId: UUID, item: WatchlistItem): Promise<void>
removeItem(watchlistId: UUID, ticker: Ticker): Promise<void>
}
interface IStrategyRepository {
findByUser(userId: UserId): Promise<Strategy[]>
findById(id: UUID): Promise<Strategy | null>
save(strategy: Strategy): Promise<void>
}
interface IAlertRepository {
findByWatchlist(watchlistId: UUID, status?: AlertStatus): Promise<Alert[]>
save(alert: Alert): Promise<void>
resolve(id: UUID): Promise<void>
dismiss(id: UUID): Promise<void>
}
interface ISectorRotationRepository {
getRotations(date: Date): Promise<SectorRotation[]>
save(rotations: SectorRotation[]): Promise<void>
getHistorical(dateFrom: Date, dateTo: Date): Promise<SectorRotation[]>
}
```