375 lines
15 KiB
Markdown
375 lines
15 KiB
Markdown
# 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[]>
|
|
}
|
|
```
|