Files
connectai/tests/buySignalWatch.test.ts
T
koriweb fa397d7fa1 v2.2.268-291: /email 통합(브리핑·스캔·워처) + stocks 자동편입·매수알림 + 지식문서·메신저 중앙화
[/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>
2026-07-03 18:57:12 +09:00

81 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});
});