# 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 PeerGroup (Aggregate Root) ├── ticker: Ticker ├── peers: List └── similarityScores: Map 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 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 ├── strategies: List └── 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 ├── 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 Screener (Aggregate Root) ├── id: UUID ├── name: string ├── description: string ├── conditions: ScreenerConditions (Value Object) ├── lastRunAt: DateTime ├── createdBy: UserId └── results: List (stored in screener_results table) ScreenerConditions (Value Object) ├── filters: List ├── 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 findByTickers(tickers: Ticker[]): Promise save(stock: Stock): Promise getSecFilings(ticker: Ticker, limit?: number): Promise getInsiderTrades(ticker: Ticker, limit?: number): Promise getPeerGroup(ticker: Ticker): Promise } interface IPriceRepository { getHistory(ticker: Ticker, from: Date, to: Date, interval: string): Promise getLatest(ticker: Ticker): Promise saveBatch(prices: PriceEvent[]): Promise getMomentum(ticker: Ticker, period: number): Promise } interface IWatchlistRepository { findByUser(userId: UserId): Promise findById(id: UUID): Promise save(watchlist: Watchlist): Promise addItem(watchlistId: UUID, item: WatchlistItem): Promise removeItem(watchlistId: UUID, ticker: Ticker): Promise } interface IStrategyRepository { findByUser(userId: UserId): Promise findById(id: UUID): Promise save(strategy: Strategy): Promise } interface IAlertRepository { findByWatchlist(watchlistId: UUID, status?: AlertStatus): Promise save(alert: Alert): Promise resolve(id: UUID): Promise dismiss(id: UUID): Promise } interface ISectorRotationRepository { getRotations(date: Date): Promise save(rotations: SectorRotation[]): Promise getHistorical(dateFrom: Date, dateTo: Date): Promise } ```