← 글 목록

notes

stock auto trading

주식 자동매매의 데이터 수집·전략·주문 실행 흐름과 운영 메모.

역할부터 정리하면 이렇게 나뉩니다.

둘은 독립입니다. 수집은 배치, 화면은 dev.


A. 수집까지 (배치만, UI 안 켜도 됨)

cd /Users/mac/develop/stock-auto-trade

# 0) 최초 1회 — DB 없을 때만 (batch가 해도 됨)
sqlite3 data/market-intel.db < data/schema/market-intel.sql

# 1) 전체 1회 (macro → symbol → news → financials → analyst)
pnpm batch:intel

# 2) 30분마다 반복 — 이 터미널은 계속 켜 둠
pnpm batch:intel:loop
  • KIS 토큰: ingest:symbol이 data/kis-oauth-cache.json에 자동 발급·~24h 갱신
  • .env에 KIS 키 필요
  • pnpm dev / kis:sync-cache 불필요

B. 웹까지 볼 때 (UI 추가)

# 터미널 1 — UI
pnpm dev
# 브라우저 <http://localhost:3000>

# 터미널 2 — 수집 (A와 동일)
pnpm batch:intel        # 최초 1회
pnpm batch:intel:loop   # 이후 30분
  • dev는 조회만 (DB 읽기 + 필요 시 API 폴백)
  • 배치가 쌓은 DB·토큰 파일을 웹이 같이 사용

C. 배치 끄고 UI만

pnpm dev
  • 조회 가능
  • DB는 마지막 수집분까지 표시, 30분 넘으면 만료 경고
  • KIS 시세는 dev가 파일 캐시 또는 직접 발급

한 줄 요약

수집 운영: batch:intel → batch:intel:loop

화면: 필요할 때만 pnpm dev

순서 강제는 없음 — 다만 최신 DB·AI 리포트를 쓰려면 배치(또는 cron)는 돌아가야 합니다.

시스템 레이어 한눈에

[설정 저장]  ─→  ai-portfolio-run-config.json
                 (거래가능 USD · 모드 · 방 유지일)
                          ↓ allocator / 게이트 / TTL이 참조

[워치 sync]  ─→  portfolio-watchlist.json
                 (Buy Intel 픽 → 게이트 통과 종목 명단)
                          ↓ batch cycle이 매수 후보로 사용

[방만 OPEN]  ─→  ws-room-config/__AI_PORTFOLIO__.json
                 + ws-engine-state.json (running, tradingEnabled)
                          ↓ ws-daemon이 45초마다 매매 루프

[intent만]   ─→  trade_intents 테이블 + KIS 주문 제출
                 (다음 45초 cycle을 기다리지 않고 즉시 1회)

각 버튼의 역할 · 필요한 이유

왜 한꺼번에 안 묶고 분리해 두는가

세 가지가 독립적으로 변할 수 있기 때문입니다. 예시:

  • 케이스 A — 점심에 펀딩이 들어와서 거래 금액만 늘림

    설정 저장만 변경 → 다음 cycle이 새 budget으로 배분 재계산. 워치·방을 건들 필요 없음. 방 재OPEN하면 진행 중인 주문이 꼬일 수 있음.

  • 케이스 B — Buy Intel을 새로 수집했는데 픽이 달라짐

    ASTS·RKLB가 새로 buy 등급, MEME가 sell 등급으로 바뀐 상황. 워치만 sync하면 다음 cycle에 ASTS 매수 + MEME 청산이 자연스레 트리거. 방·설정은 안 건드림.

  • 케이스 C — 데몬 재시작 후 방이 자동 복구 안 됨

    ws-engine-state.json엔 stopped로 남고 워치리스트는 그대로. 방만 OPEN으로 시세 구독·매매 루프 재가동. 워치·설정 sync 불필요.

  • 케이스 D — FOMC 회의 직후 즉시 포지션 조정

    다음 45초 cycle을 기다리지 않고 intent만 적용으로 즉시 매도·매수 평가.

만약 4개를 단일 버튼으로만 노출하면:

  • 케이스 A·B·C에서 불필요한 작업이 함께 실행 (방 재OPEN으로 진행 중인 주문 중단, 워치 sync로 노이즈 발생 등)
  • 디버깅 시 어디서 꼬였는지 분리 불가

일반 사용자가 알아야 할 것

평소 운영은 이거 1개만 누르세요:

🟢 AI 포트폴리오 시작 — 워치 sync → 방 OPEN → intent 적용까지 자동 진행

「고급」 배지가 붙은 나머지 3개 + intent만 적용은:

  • 자동 매매가 이미 잘 돌고 있는 중에 부분만 갱신하고 싶을 때
  • 디버깅·운영 트러블슈팅
  • "방은 살아 있는데 워치만 새로 sync 하고 싶다" 같은 정밀 조정

따라서 숨기지 않고 작은 사이즈로 노출만 해뒀습니다 — 일반 사용자는 안 봐도 되고, 운영자는 필요할 때 바로 누를 수 있도록.

정리

이게 4개 버튼이 따로 존재하는 이유입니다. 다만 일반적인 시작·재시작 시나리오에서는 그냥 「AI 포트폴리오 시작」 하나만 누르시면 모두 처리됩니다.


1. Tailscale — 정확히 뭐고 왜 쓰는가

한 줄 요약

"내 디바이스만 가입할 수 있는 사설 VPN 네트워크" — 외부 인터넷에 포트를 단 하나도 열지 않고도, 본인 계정에 등록된 폰/맥/노트북끼리만 서로 직접 IP로 통신할 수 있게 만듭니다.

동작 원리 (개념)

외부 인터넷(공격자) ──❌── [맥의 공유기·방화벽]
                            │
                            └── 포트 닫혀있음, 외부 접근 불가

당신의 디바이스 ──✅── Tailscale 사설망 (100.x.x.x) ──✅── [맥의 pm2 / next:3000]
   (폰·노트북)         WireGuard 암호화 P2P

핵심:

  • 외부에 포트 노출 안 함. 공유기 설정 손댈 필요 없음. 공인 IP/도메인 불필요.
  • 계정 단위로 디바이스가 묶임. 본인이 admin 콘솔에서 추가 승인 안 한 디바이스는 절대 못 들어옴.
  • 각 디바이스에 100.x.y.z 형식의 가상 IP가 부여됨. 어디서든(LTE/외부 와이파이/지구 반대편) 그 IP로 접근 가능.
  • 모든 트래픽은 WireGuard 암호화 + 가능하면 P2P 직결(중계 안 거치고 디바이스 ↔ 디바이스).
  • 비교: 다른 옵션과 뭐가 다른가

    셋업 (5분)

    맥 (서버 쪽):

    brew install --cask tailscale
    open -a Tailscale          # 메뉴바 아이콘 클릭 → "Log in" → Google/Apple/이메일 인증
    tailscale ip -4            # 이 맥의 Tailscale IP 확인 (예: 100.64.12.34)
    tailscale status           # MagicDNS 이름 확인 (예: kms-mac)

    폰 (클라이언트):

  • App Store에서 Tailscale 앱 설치
  • 같은 계정 로그인 → 자동 등록
  • Tailscale 토글 ON
  • 접속:

  • http://100.64.12.34:3000 (IP)
  • 또는 http://kms-mac:3000 (MagicDNS — 외우기 쉬움)
  • 추가 옵션 (선택)

  • HTTPS: tailscale serve --https=443 http://localhost:3000 한 줄로 자동 인증서 발급 → https://kms-mac.tail-scale.ts.net 형태로 접속 가능
  • Funnel: 가입 안 한 외부인에게 잠깐 보여줄 때 (tailscale funnel) — 본인 요구사항엔 불필요

  • 2. ecosystem.config.cjs — 각 옵션 뭐 하는가

    ecosystem.config.cjs의 핵심 블록을 옵션별로 풀어 설명하겠습니다.

    apps 배열 — 한 ecosystem에서 여러 프로세스를 동시에 정의

    두 개의 앱이 들어 있습니다:

    옵션별 의미

          name: "stock-auto-web",
          cwd: __dirname,
          script: "pnpm",
          args: `start -- -H 0.0.0.0 -p ${PORT}`,
          interpreter: "none",
          autorestart: true,
          max_restarts: 10,
          restart_delay: 3000,
          max_memory_restart: "1G",
          kill_timeout: 5000,
          env: baseEnv,
          out_file: path.join(__dirname, "logs/pm2-web.out.log"),
          error_file: path.join(__dirname, "logs/pm2-web.err.log"),
          merge_logs: true,
          time: true,

    loop 앱은 뭐가 다른가

          name: "stock-auto-loop",
          cwd: __dirname,
          script: "pnpm",
          args: "run all:loop",
          interpreter: "none",
          autorestart: true,
          max_restarts: 10,
          restart_delay: 5000,
          kill_timeout: 10000,
          env: baseEnv,
          out_file: path.join(__dirname, "logs/pm2-loop.out.log"),
          error_file: path.join(__dirname, "logs/pm2-loop.err.log"),
          merge_logs: true,
          time: true,
  • args: "run all:loop" — 30분 주기로 batch:intel + buy-intel:live를 도는 무한 루프 스크립트
  • kill_timeout: 10000 — 10초. 인텔 수집 중간에 끊지 않도록 web(5초)보다 길게 잡음
  • max_memory_restart 없음 — 큰 데이터 다룰 일이 별로 없고 무한 루프 자체가 메모리 누적 안 됨
  • 부가 메커니즘

    pm2 save + pm2 startup

  • pm2 save: 현재 실행 중인 프로세스 목록을 ~/.pm2/dump.pm2에 저장
  • pm2 startup: macOS의 launchd에 PM2 자체를 등록. 부팅 시 launchd가 PM2를 띄우고, PM2가 저장된 목록을 자동 복원
  • → 결과적으로 재부팅·로그인 시 두 앱 자동 시작
  • 코드 변경 → 무중단 배포

    pnpm build
    pm2 reload stock-auto-web      # graceful — 이전 요청 끝낸 후 새 프로세스로 교체

    loop는 reload보다 restart가 안전:

    pm2 restart stock-auto-loop

    포트 바꿀 때

    PORT=8080 pnpm pm2:restart     # ecosystem이 process.env.PORT를 읽어서 반영

    다음 단계 권장 순서

  • npm i -g pm2 (pm2 글로벌 설치)
  • dev 서버(Ctrl+C) + pnpm all:loop 백그라운드 종료
  • pnpm build (이미 한 번 통과 — 변경 없으면 스킵 가능)
  • pnpm pm2:start — 두 프로세스 기동
  • pm2 startup → 출력된 sudo 명령 복붙 → pm2 save
  • pnpm pm2:status로 둘 다 online 확인
  • brew install --cask tailscale → 로그인 → 폰 앱 설치 → 같은 계정 로그인
  • 폰에서 http://<MagicDNS이름>:3000 접속 확인