Initial commit: invest-copilot app
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
# 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[]>
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user