# Deeper Brief Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** `get_brief` (MCP)가 주간 포트폴리오 성과, 종목별 주간 추세, 경제 지표 캘린더를 포함하도록 `BriefData`를 확장해 Claude가 현상태 + 1주일 전략을 브리핑할 수 있게 한다.

**Architecture:** Finnhub에 경제 캘린더 API 메서드를 추가하고, `packages/brief`의 타입과 조립 로직만 수정한다. 기존 `BriefData`에 3개 필드(`week_performance`, `economic_calendar`, `BriefHolding.week_change_pct`)를 추가하는 것이 전부다. MCP 툴 코드(`apps/mcp/src/index.ts`)는 변경 없음 — `assembleBriefData` 결과를 그대로 반환하므로 자동으로 반영된다.

**Tech Stack:** TypeScript, `@firma/finnhub` (Finnhub REST), `@firma/db` Snapshot 테이블, Drizzle types

---

## 배경: 현재 BriefData 구조

```
packages/brief/src/types.ts       — BriefData, BriefHolding 등 타입 정의
packages/brief/src/assemble.ts    — assembleBriefData() 조립 로직
packages/finnhub/src/types.ts     — Finnhub API 응답 타입
packages/finnhub/src/index.ts     — createFinnhubClient() 팩토리
```

**Snapshot 스키마** (`packages/db/src/schema.ts:38`):
```
date: text, ticker: text, shares: real, avg_price: real, current_price: real, currency: text
```
→ 날짜별·종목별로 저장되므로 7일 전 스냅샷과 현재 가격을 비교하면 주간 종목별 수익률을 계산할 수 있다.

**기존 snapshot_comparison**: 마지막 스냅샷 날짜 기준 비교. `week_performance`는 "오늘로부터 7일 전 날짜에 가장 가까운 스냅샷" 기준으로 동일 구조를 사용한다.

---

## Task 1: Finnhub — 경제 캘린더 타입 추가

**Files:**
- Modify: `packages/finnhub/src/types.ts`
- Modify: `packages/finnhub/src/index.ts`

### Step 1: EconomicEvent, EconomicCalendarResponse 타입 추가

`packages/finnhub/src/types.ts` 파일 맨 아래에 추가:

```typescript
export type EconomicEvent = {
  actual: number | null;
  country: string;
  estimate: number | null;
  event: string;
  impact: string;
  prev: number | null;
  time: string;
  unit: string;
};

export type EconomicCalendarResponse = {
  economicCalendar: EconomicEvent[];
};
```

### Step 2: index.ts에 import 및 메서드 추가

`packages/finnhub/src/index.ts` 상단 import에 두 타입 추가:

```typescript
import type {
  Metric, Profile, Quote, StockData,
  NewsItem, InsiderTransactionsResponse, EarningsCalendarResponse,
  FinancialsReportedResponse, CandleResponse,
  EconomicCalendarResponse,  // ← 추가
} from './types.ts';
```

`createFinnhubFetcher` 내 `return` 블록에 메서드 추가:

```typescript
getEconomicCalendar: (from: string, to: string) =>
  get<EconomicCalendarResponse>('/calendar/economic', { from, to }),
```

`createFinnhubClient` return 블록에도 노출:

```typescript
getEconomicCalendar: fetcher.getEconomicCalendar,
```

### Step 3: index.ts export에 타입 추가

`packages/finnhub/src/index.ts` 상단 `export type` 블록에 추가:

```typescript
export type {
  StockData, NewsItem, InsiderTransaction, InsiderTransactionsResponse,
  EarningsItem, EarningsCalendarResponse,
  FinancialLineItem, FinancialReport, FinancialPeriod, FinancialsReportedResponse,
  CandleResponse,
  EconomicEvent, EconomicCalendarResponse,  // ← 추가
} from './types.ts';
```

### Step 4: 타입체크

```bash
cd /Users/evan/dev/playground/firma && yarn typecheck
```

Expected: pass (finnhub 패키지 변경만).

### Step 5: 커밋

```bash
git add packages/finnhub/src/types.ts packages/finnhub/src/index.ts
git commit -m "feat(finnhub): add economic calendar API method"
```

---

## Task 2: Brief — 타입 확장

**Files:**
- Modify: `packages/brief/src/types.ts`

### Step 1: BriefEconomicEvent 타입 추가

`packages/brief/src/types.ts`에서 `import type { MacroResult, StressIndex, Regime } from '@firma/fred';` 아래에 추가:

```typescript
export type BriefEconomicEvent = {
  event: string;
  country: string;
  time: string;
  impact: string;
  actual: number | null;
  estimate: number | null;
  prev: number | null;
  unit: string;
};

export type BriefWeekPerformance = {
  baseline_date: string | null;
  baseline_value_usd: number;
  current_value_usd: number;
  change_usd: number;
  change_pct: number;
};
```

### Step 2: BriefHolding에 week_change_pct 추가

기존 `BriefHolding` 타입에 필드 추가:

```typescript
export type BriefHolding = {
  ticker: string;
  shares: number;
  current_price: number;
  prev_close: number;
  avg_price: number | null;
  market_value: number;
  weight_pct: number;
  pnl_today_usd: number;
  pnl_today_pct: number;
  pnl_total_usd: number | null;
  pnl_total_pct: number | null;
  sector: string | null;
  country: string | null;
  currency: string;
  week_change_pct: number | null;  // ← 추가
};
```

### Step 3: BriefData에 두 필드 추가

```typescript
export type BriefData = {
  date: string;
  generated_at: string;
  portfolio: { ... };  // 기존 그대로
  holdings: BriefHolding[];
  concentration: BriefConcentration | null;
  movers: { winners: BriefMover[]; losers: BriefMover[] };
  news: BriefNewsItem[];
  earnings_upcoming: BriefEarnings[];
  macro: BriefMacro | null;
  signals: BriefSignals | null;
  insights: BriefInsight[];
  snapshot_comparison: BriefSnapshotComparison;
  week_performance: BriefWeekPerformance;           // ← 추가
  economic_calendar: BriefEconomicEvent[];          // ← 추가
};
```

### Step 4: 타입체크

```bash
cd /Users/evan/dev/playground/firma && yarn typecheck
```

Expected: `assemble.ts`에서 `week_performance`, `economic_calendar` 필드 누락 오류 발생 — 의도된 실패.

### Step 5: 커밋

```bash
git add packages/brief/src/types.ts
git commit -m "feat(brief): add week_performance and economic_calendar types"
```

---

## Task 3: Brief — 주간 성과 계산 로직

**Files:**
- Modify: `packages/brief/src/assemble.ts`

### Step 1: computeWeekPerformance 함수 추가

`assemble.ts`의 `computeSnapshotComparison` 함수(line 31) 바로 아래에 추가:

```typescript
const computeWeekPerformance = (
  snapshots: Snapshot[],
  today: string,
  currentValue: number,
): BriefWeekPerformance => {
  const sevenDaysAgo = new Date(Date.now() - 7 * 86_400_000).toISOString().slice(0, 10);

  // 7일 전 날짜 이하의 스냅샷 중 가장 최근 것
  const baselineDate = snapshots
    .map(s => s.date)
    .filter(d => d < today && d <= sevenDaysAgo)
    .reduce<string | null>((latest, d) => (latest == null || d > latest ? d : latest), null);

  if (baselineDate == null) {
    return {
      baseline_date:      null,
      baseline_value_usd: currentValue,
      current_value_usd:  currentValue,
      change_usd:         0,
      change_pct:         0,
    };
  }

  const baselineValue = snapshots
    .filter(s => s.date === baselineDate)
    .reduce((sum, s) => sum + s.current_price * s.shares, 0);
  const change = currentValue - baselineValue;
  const changePct = baselineValue > 0 ? (change / baselineValue) * 100 : 0;

  return {
    baseline_date:      baselineDate,
    baseline_value_usd: baselineValue,
    current_value_usd:  currentValue,
    change_usd:         change,
    change_pct:         changePct,
  };
};
```

### Step 2: enrichHoldingsWithWeekChange 함수 추가

`computeWeekPerformance` 바로 아래에 추가:

```typescript
const enrichHoldingsWithWeekChange = (
  holdings: BriefHolding[],
  snapshots: Snapshot[],
  today: string,
): BriefHolding[] => {
  const sevenDaysAgo = new Date(Date.now() - 7 * 86_400_000).toISOString().slice(0, 10);

  const baselineDate = snapshots
    .map(s => s.date)
    .filter(d => d < today && d <= sevenDaysAgo)
    .reduce<string | null>((latest, d) => (latest == null || d > latest ? d : latest), null);

  if (baselineDate == null) return holdings.map(h => ({ ...h, week_change_pct: null }));

  const baselinePriceByTicker = new Map(
    snapshots
      .filter(s => s.date === baselineDate)
      .map(s => [s.ticker, s.current_price]),
  );

  return holdings.map(h => {
    const baselinePrice = baselinePriceByTicker.get(h.ticker);
    const week_change_pct =
      baselinePrice != null && baselinePrice > 0
        ? ((h.current_price - baselinePrice) / baselinePrice) * 100
        : null;
    return { ...h, week_change_pct };
  });
};
```

### Step 3: 타입 import 추가

`assemble.ts` 상단 import에 `BriefWeekPerformance` 추가:

```typescript
import type {
  BriefData, BriefHolding, BriefConcentration, BriefMover, BriefNewsItem,
  BriefEarnings, BriefMacro, BriefSignals, BriefInsight, BriefSnapshotComparison,
  BriefWeekPerformance,  // ← 추가
} from './types.js';
```

### Step 4: 타입체크

```bash
cd /Users/evan/dev/playground/firma && yarn typecheck
```

Expected: `economic_calendar` 누락 오류만 남아야 함.

### Step 5: 커밋

```bash
git add packages/brief/src/assemble.ts
git commit -m "feat(brief): add week performance and per-holding week trend computation"
```

---

## Task 4: Brief — 경제 캘린더 조립

**Files:**
- Modify: `packages/brief/src/assemble.ts`

### Step 1: assembleEconomicCalendar 함수 추가

`assembleBriefSignals` 함수(line 284) 바로 아래에 추가:

```typescript
const assembleEconomicCalendar = async (
  client: ReturnType<typeof createFinnhubClient>,
  from: string,
  to: string,
): Promise<BriefEconomicEvent[]> => {
  try {
    const res = await client.getEconomicCalendar(from, to);
    return (res.economicCalendar ?? [])
      .filter(e => e.impact === 'high' || e.impact === 'medium')
      .map(e => ({
        event:    e.event,
        country:  e.country,
        time:     e.time,
        impact:   e.impact,
        actual:   e.actual,
        estimate: e.estimate,
        prev:     e.prev,
        unit:     e.unit,
      }))
      .sort((a, b) => a.time.localeCompare(b.time));
  } catch {
    return [];
  }
};
```

### Step 2: import에 BriefEconomicEvent 추가

```typescript
import type {
  BriefData, BriefHolding, BriefConcentration, BriefMover, BriefNewsItem,
  BriefEarnings, BriefMacro, BriefSignals, BriefInsight, BriefSnapshotComparison,
  BriefWeekPerformance, BriefEconomicEvent,  // ← BriefEconomicEvent 추가
} from './types.js';
```

### Step 3: assembleBriefData에 week_performance, economic_calendar 추가

`assembleBriefData` 함수(line 298) 내부를 아래와 같이 수정:

**3-1.** `computeHoldings` 결과로 holdings를 구한 뒤 바로 enrichment 추가 (기존 line 306 이후):

```typescript
const { holdings: rawHoldings, totalValue, totalCost, totalDailyChange, prevTotalValue } = computeHoldings(deps.transactions, priceMap);
const holdings = enrichHoldingsWithWeekChange(rawHoldings, deps.snapshots, date);
```

기존 코드: `const { holdings, ... } = computeHoldings(...)` → 위처럼 두 줄로 교체.

**3-2.** `earnings_upcoming` 조립 블록 (line 344) 내, Finnhub client를 이미 생성한 위치에서 경제 캘린더 fetch 추가:

```typescript
// 기존 client 생성 직후에 추가
const horizon7 = localDate(addDays(new Date(), 7));
const economicCalendarPromise = assembleEconomicCalendar(client, date, horizon7);
```

그리고 `earnings_upcoming` 조립 완료 후:
```typescript
economic_calendar = await economicCalendarPromise;
```

변수를 블록 밖에서도 쓸 수 있도록 선언 위치 조정 필요. 아래처럼 리팩토링:

```typescript
let news: BriefNewsItem[] = [];
let earnings_upcoming: BriefEarnings[] = [];
let economic_calendar: BriefEconomicEvent[] = [];

if (deps.finnhubKey && tickers.length > 0) {
  const client = createFinnhubClient(deps.finnhubKey);
  const yest = localDate(addDays(new Date(), -1));
  const horizon = localDate(addDays(new Date(), earningsHorizonDays));
  const horizon7 = localDate(addDays(new Date(), 7));

  const [newsResults, earningsResults, calendarResult] = await Promise.all([
    Promise.all(
      tickers.map(t =>
        client.getCompanyNews(t, yest, date)
          .then(items => items.slice(0, newsPerTicker).map((n: NewsItem): BriefNewsItem => ({
            ticker: t, headline: n.headline, summary: n.summary, source: n.source,
            published_at: n.datetime, url: n.url,
          })))
          .catch(() => [] as BriefNewsItem[]),
      ),
    ),
    Promise.all(
      tickers.map(t =>
        client.getEarningsCalendar(date, horizon, t)
          .then(r => r.earningsCalendar ?? [])
          .catch(() => [] as EarningsItem[]),
      ),
    ),
    assembleEconomicCalendar(client, date, horizon7),
  ]);

  news = newsResults.flat().sort((a, b) => b.published_at - a.published_at);

  const weightByTicker = new Map(holdings.map(h => [h.ticker, h.weight_pct]));
  earnings_upcoming = earningsResults.flat()
    .map(e => ({ ...e, weight_pct: weightByTicker.get(e.symbol) ?? null }))
    .sort((a, b) => a.date.localeCompare(b.date));

  economic_calendar = calendarResult;
}
```

**3-3.** `snapshot_comparison` 계산 직후에 `week_performance` 추가:

```typescript
const snapshot_comparison = computeSnapshotComparison(deps.snapshots, date, totalValue);
const week_performance = computeWeekPerformance(deps.snapshots, date, totalValue);
```

**3-4.** `partial` 객체에 두 필드 추가:

```typescript
const partial: Omit<BriefData, 'insights' | 'generated_at'> = {
  date,
  portfolio: { ... },
  holdings,
  concentration,
  movers: { winners, losers },
  news,
  earnings_upcoming,
  macro,
  signals,
  snapshot_comparison,
  week_performance,        // ← 추가
  economic_calendar,       // ← 추가
};
```

### Step 4: 타입체크

```bash
cd /Users/evan/dev/playground/firma && yarn typecheck
```

Expected: 오류 없음.

### Step 5: 빌드 확인

```bash
cd /Users/evan/dev/playground/firma && yarn build
```

Expected: 빌드 성공.

### Step 6: 커밋

```bash
git add packages/brief/src/assemble.ts
git commit -m "feat(brief): wire week_performance and economic_calendar into assembleBriefData"
```

---

## Task 5: MCP 캐시 schema guard 업데이트

MCP 툴(`apps/mcp/src/index.ts`)에는 캐시 유효성 검사에서 `snapshot_comparison` 필드 존재 여부를 체크하는 schema guard가 있다. `week_performance`도 누락된 구버전 캐시를 무효화하도록 수정한다.

**Files:**
- Modify: `apps/mcp/src/index.ts`

### Step 1: schema guard 수정

`apps/mcp/src/index.ts`에서 아래 패턴을 찾아 수정:

현재 코드 (line ~290 부근):
```typescript
if (!cached.snapshot_comparison) {
  refresh = true;
}
```

수정 후:
```typescript
if (!cached.snapshot_comparison || !cached.week_performance) {
  refresh = true;
}
```

### Step 2: 타입체크 + 빌드

```bash
cd /Users/evan/dev/playground/firma && yarn typecheck && yarn build
```

Expected: 오류 없음.

### Step 3: 커밋

```bash
git add apps/mcp/src/index.ts
git commit -m "feat(mcp): invalidate brief cache missing week_performance field"
```

---

## 완료 후 확인

MCP 클라이언트(Claude)에서 `get_brief`를 호출하면 응답에:
- `week_performance`: 7일 전 대비 포트폴리오 성과
- `economic_calendar`: 향후 7일 고/중 임팩트 경제 이벤트 목록
- `holdings[n].week_change_pct`: 종목별 주간 수익률

이 포함되어 있어야 한다. Claude는 이 데이터를 활용해 현재 상태 + 1주일 전략을 자연어로 브리핑할 수 있다.
