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핵심:
100.x.y.z 형식의 가상 IP가 부여됨. 어디서든(LTE/외부 와이파이/지구 반대편) 그 IP로 접근 가능.비교: 다른 옵션과 뭐가 다른가
셋업 (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)폰 (클라이언트):
접속:
http://100.64.12.34:3000 (IP)http://kms-mac:3000 (MagicDNS — 외우기 쉬움)추가 옵션 (선택)
tailscale serve --https=443 http://localhost:3000 한 줄로 자동 인증서 발급 → https://kms-mac.tail-scale.ts.net 형태로 접속 가능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 글로벌 설치)pnpm all:loop 백그라운드 종료pnpm build (이미 한 번 통과 — 변경 없으면 스킵 가능)pnpm pm2:start — 두 프로세스 기동pm2 startup → 출력된 sudo 명령 복붙 → pm2 savepnpm pm2:status로 둘 다 online 확인brew install --cask tailscale → 로그인 → 폰 앱 설치 → 같은 계정 로그인http://<MagicDNS이름>:3000 접속 확인