Files

15 KiB

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)

// 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[]>
}