fa397d7fa1
[/email — Gmail + 클래식 Outlook (신규 기능군)]
- 기본 명령: brief(PM 업무 허브 대시보드)·list·read·wikify·reply(Drafts만)·open(Outlook 창 열기)
- Gmail: 자체 OAuth gmail.readonly 스코프 추가 (재연결 1회 필요) / Outlook: COM(PowerShell), 전 계정(Store)·하위 폴더(120개 가드) 수집, To/CC 판별
- 브리핑: 상태 3분류(결정/회신/확인만)·P등급 영향·프로젝트 위험도·Today Focus·오늘 완료 예상·기간 명시 시 표시 범위 제한
- 영속 상태(email_state.json): 회신대기/공넘김 상태머신, 창 밖 미회신 보존, done old N 일괄정리
- 워처: 주기 자동 수집 + 미회신 임계 알림(기본 끔) + 데일리 브리핑 '답할 이메일' 섹션
- 전체 스캔(/email scan): 자동 위키화(회차 15건·레지스트리·파일삭제 감지 재생성)·진행 표시(streamProgress)
- 자동발신 필터 확장(Confluence·AWS·광고·[Notification]) + 라벨 오타 후처리(polishBriefLabels)
[/stocks]
- discover 자동 편입(stocks.json) + 매수권장가 224일선 자동 산출
- 매수사정권 진입 전이 감지 → 텔레그램 1회 알림 (직전신호 추적)
- 버그 수정: '미충족'이 .includes('충족')에 오탐되던 필터 판정
[지식 문서 파이프라인 통일 (wikiFormat/wikiSave)]
- PC 로컬 직접 저장 (NAS 볼륨 미아 문제 해소) — 설정 폴더 → 두뇌 → 워크스페이스
- 이메일 전용 Email_Wiki 분리(email.wikifySavePath) + 봉투(frontmatter) 자동 통일
- 정본 프롬프트 단일 빌더(buildKnowledgeWikiPrompt) — web/email 래퍼화, 템플릿 fetch 일원화
- 이메일 문서 시점 3중 방어: 기준시점 헤더·제목=메일 최신일·날짜 없는 현재형 금지
- docs/WIKI_DOC_PIPELINE.md 개발 규약
[텔레그램 메신저 중앙화 (messageFormat)]
- 단일 렌더러 renderTelegram — 5개 발신 빌더(주식보고·매수시그널·Top5·데일리·미회신) 통일
- 주식 보고 시각 하드코딩 제거(messenger.stocksReportTimes CSV) + 알림 토글 2종
- 설정 패널 '메신저' 탭 신설 · '이메일' 토글 섹션(Gmail/Outlook on/off)
테스트 803개 통과 (이메일 상태머신·CC판별·포맷·매수전이·봉투 등 60+ 신규)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
81 lines
3.7 KiB
TypeScript
81 lines
3.7 KiB
TypeScript
/**
|
||
* v2.2.268 — 발굴 자동 편입 + 매수 시그널 진입 알림.
|
||
* - computeRecommendedBuy: 224일선 기반 매수권장가 자동 산출 규칙
|
||
* - detectBuyZoneEntries: 관망/고평가 → 매수사정권 전이 감지 + 중복 알림 방지
|
||
*/
|
||
import { computeRecommendedBuy } from '../src/features/stocks/stockDiscovery';
|
||
import { detectBuyZoneEntries } from '../src/features/stocks/signalClassifier';
|
||
import type { Stock } from '../src/features/stocks/types';
|
||
|
||
describe('computeRecommendedBuy — 224일선 기반 매수권장가', () => {
|
||
test('현재가 > MA224 → MA224 (지지선 회귀 시 매수)', () => {
|
||
expect(computeRecommendedBuy(12000, 10000)).toBe(10000);
|
||
});
|
||
|
||
test('현재가 ≤ MA224 → 현재가 × 0.97', () => {
|
||
expect(computeRecommendedBuy(9000, 10000)).toBe(Math.round(9000 * 0.97));
|
||
});
|
||
|
||
test('MA224 없음(시세 부족) → 현재가 × 0.90 폴백', () => {
|
||
expect(computeRecommendedBuy(10000, undefined)).toBe(9000);
|
||
});
|
||
|
||
test('현재가 없음/비정상 → undefined (권장가 미설정)', () => {
|
||
expect(computeRecommendedBuy(undefined, 10000)).toBeUndefined();
|
||
expect(computeRecommendedBuy(0, 10000)).toBeUndefined();
|
||
expect(computeRecommendedBuy(-100)).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('detectBuyZoneEntries — 매수사정권 진입 전이 감지', () => {
|
||
const stock = (over: Partial<Stock>): Stock => ({
|
||
이름: '테스트', 심볼: '000001',
|
||
'3/4 필터': '[자동 평가] 충족 (ROE, 성장성, 유동성)',
|
||
현재가: 9000, 매수권장가: '10,000',
|
||
...over,
|
||
});
|
||
|
||
test('신규 진입 (직전신호 없음 + 현재가 ≤ 권장가) → 알림 대상', () => {
|
||
const store = [stock({})];
|
||
const entered = detectBuyZoneEntries(store);
|
||
expect(entered).toHaveLength(1);
|
||
expect(entered[0].signal).toBe('BUY_ZONE');
|
||
expect(store[0].직전신호).toBe('BUY_ZONE');
|
||
});
|
||
|
||
test('직전에도 매수사정권이었으면 중복 알림 안 함', () => {
|
||
const store = [stock({ 직전신호: 'BUY_ZONE' })];
|
||
expect(detectBuyZoneEntries(store)).toHaveLength(0);
|
||
expect(store[0].직전신호).toBe('BUY_ZONE');
|
||
});
|
||
|
||
test('고평가 → 매수사정권 전이 → 알림 + 직전신호 갱신', () => {
|
||
const store = [stock({ 직전신호: 'OVERVALUED' })];
|
||
const entered = detectBuyZoneEntries(store);
|
||
expect(entered).toHaveLength(1);
|
||
expect(store[0].직전신호).toBe('BUY_ZONE');
|
||
});
|
||
|
||
test('필터 미충족(관망)은 가격이 권장가 아래여도 알림 없음', () => {
|
||
const store = [stock({ '3/4 필터': '[자동 평가] 미충족 (사유: …)', 직전신호: 'HOLD' })];
|
||
expect(detectBuyZoneEntries(store)).toHaveLength(0);
|
||
expect(store[0].직전신호).toBe('HOLD');
|
||
});
|
||
|
||
test('매수권장가 없으면 BUY_ZONE 불가 → 알림 없음 (자동 산출의 존재 이유)', () => {
|
||
const store = [stock({ 매수권장가: undefined })];
|
||
expect(detectBuyZoneEntries(store)).toHaveLength(0);
|
||
expect(store[0].직전신호).toBe('OVERVALUED');
|
||
});
|
||
|
||
test('매수사정권 이탈(가격 상승) → 직전신호가 OVERVALUED 로 → 재진입 시 다시 알림', () => {
|
||
const store = [stock({})];
|
||
detectBuyZoneEntries(store); // 1차 진입
|
||
store[0].현재가 = 12000; // 권장가 위로 이탈
|
||
expect(detectBuyZoneEntries(store)).toHaveLength(0);
|
||
expect(store[0].직전신호).toBe('OVERVALUED');
|
||
store[0].현재가 = 9500; // 재진입
|
||
expect(detectBuyZoneEntries(store)).toHaveLength(1);
|
||
});
|
||
});
|