브라우저에서 동작하는 정적 웹 기반 주식 시뮬레이터입니다. 실제 증권사 WTS 화면처럼 감시종목, 시세, 호가, 체결, 차트, 주문 티켓, 자산 현황을 한 화면에 배치했고, 국내장과 미국장을 번갈아 운영하는 게임형 거래 경험을 제공합니다.
- 배포 URL: https://developlee20.github.io/
- 현재 기준 엔트리 포인트:
index.html - 현재 기준 런타임:
js/main.js에서 ES module 방식으로 각 기능 모듈을 로드 - 프레임워크/빌드 도구: 없음. 순수 HTML/CSS/Vanilla JavaScript
이 README는 새로운 개발자가 프로젝트에 바로 합류할 수 있도록 아래 내용을 빠르게 이해하게 만드는 온보딩 문서입니다.
- 이 프로젝트가 무엇인지
- 실제로 어떤 기능이 구현되어 있는지
- 어떤 파일이 현재 사용 중인지
- 상태 저장과 시뮬레이션이 어떻게 돌아가는지
- 어디를 수정하면 어떤 기능에 영향이 가는지
중요: 이 문서는 index.html + js/main.js 경로를 기준으로 작성했습니다. index_old.html, index.html.backup, js/ai-trader.js, js/ai-trader-manager.js는 현재 메인 런타임에서 사용되지 않는 레거시/보조 자산입니다.
| 항목 | 내용 |
|---|---|
| 제품 성격 | 게임형 주식 거래 시뮬레이터 |
| 실행 환경 | 브라우저 |
| 배포 방식 | GitHub Pages |
| 기술 스택 | HTML, CSS, Vanilla JS, Canvas, localStorage |
| 핵심 도메인 | 종목 생성, 시장 개폐, 주문장, 가상 트레이더, 현물/레버리지/숏, 환전, 차트 |
| 데이터 저장 | 브라우저 localStorage |
| 서버 연동 | 없음 |
| 테스트 인프라 | 없음 |
이 프로젝트는 빌드가 필요 없는 정적 사이트입니다. 다만 type="module"을 사용하므로 브라우저에서 파일을 더블클릭해 여는 방식보다 간단한 정적 서버로 실행하는 편이 안전합니다.
예시:
# Python이 있을 때
python -m http.server 4173또는
# Node가 있을 때
npx serve .그 다음 브라우저에서 명령이 출력한 로컬 주소로 접속하면 됩니다. 예를 들어 Python 예시는 http://localhost:4173입니다.
- 상태는
localStorage에 저장됩니다. - 브라우저를 새로고침해도 자산, 종목, 가상 트레이더 상태가 유지됩니다.
- 데이터 모델을 바꾼 뒤 이상 동작이 보이면 브라우저 스토리지를 비우거나 화면 우측 상단의
인생 리셋버튼으로 초기화해야 합니다.
사용자는 다음 흐름으로 플레이합니다.
- 국내/미국 감시종목 중 하나를 선택합니다.
- 선택 종목의 현재가, 직전 변동률, 시가총액, 스프레드, 최근 체결량, 내 보유 상태를 확인합니다.
- 호가창과 최근 체결을 보면서 현물 주문 또는 레버리지/숏 포지션에 진입합니다.
- 원화/달러를 환전해 시장별 예수금을 조정합니다.
- 보유 현물, 레버리지 포지션, 숏 포지션의 손익을 실시간으로 확인합니다.
- 가상 트레이더 랭킹을 보며 시장 상황을 간접적으로 파악합니다.
- 게임 오버 UI와 재시작 흐름은 존재하지만, 현재 코드에서는 자동 게임 오버 판정이 메인 루프에 연결되어 있지 않습니다.
이 프로젝트의 시장 시간은 실제 한국/미국 증시 시간과 연결되어 있지 않습니다. 사용자의 브라우저 현재 시간을 기반으로 작동하는 게임 규칙입니다.
| 규칙 | 구현 방식 |
|---|---|
| 시간 기준 | 브라우저의 new Date() |
| 국내장 개장 | 현재 분(minute)이 10분 주기 중 0~4분일 때 |
| 미국장 개장 | 현재 분(minute)이 10분 주기 중 5~9분일 때 |
| 상태 체크 주기 | 1초마다 |
| 시세 시뮬레이션 주기 | 시장이 열려 있는 동안 2초마다 |
| 환율 변동 주기 | 5초마다 |
해석하면 다음과 같습니다.
- 매 10분 사이클에서 앞 5분은 국내장, 뒤 5분은 미국장이 열립니다.
- 두 시장은 번갈아 열리며, 각각 개장/폐장 상태를 별도로 유지합니다.
- 헤더에는 현재 개장 여부와 함께
이번 5분 변동또는직전 5분 변동이 표시됩니다.
- 게임 시작 시 국내 10종목, 미국 10종목을 생성합니다.
- 종목명은 코드에 하드코딩된 가상 이름 목록에서 중복 없이 선택됩니다.
| 항목 | 국내 주식 | 미국 주식 |
|---|---|---|
| 초기 가격 범위 | 1,000원 ~ 100,000원 | 약 $10 ~ $59 |
| 호가 단위 | 100원 | $0.01 |
| 총 발행 주식 수 | 1,000주 ~ 10,000주 | 1,000주 ~ 10,000주 |
- 국내 종목은
15분마다 신규 종목 생성 시도를 합니다. - 미국 종목은
20분마다 신규 종목 생성 시도를 합니다. - 단, 이름 목록이 소진되면 더 이상 생성되지 않습니다.
현재 코드 기준 상장 가능 이름 수:
- 국내: 10개
- 미국: 11개
즉, 기본 시작에서 국내는 사실상 이미 최대치이고, 미국은 추가로 1종목 더 상장될 여지가 있습니다.
- 모든 종목은
priceHistory배열을 가집니다. - 차트는 Canvas로 직접 그립니다.
- 소형 차트는 본문 중앙 패널에 노출됩니다.
- 상세 차트는 모달에서 별도로 렌더링됩니다.
- 가격 이력은 최대 100개 포인트까지만 유지합니다.
- 차트 색상 팔레트는 다크 모드 여부에 따라 바뀝니다.
차트는 최근 체결가 흐름을 보여주는 성격이며, 별도 라이브러리를 사용하지 않습니다.
이 프로젝트의 핵심은 단순 랜덤 가격 변동이 아니라, 가상 트레이더가 만든 주문장을 바탕으로 체결이 발생한다는 점입니다.
- 주문장은
orderBook[marketId][stockId]구조를 사용합니다. - 체결 내역은
tradeHistory[marketId][stockId]에 저장합니다. - 주문 정렬 원칙은
가격 우선 -> 시간 우선입니다.
- 시장이 열려 있는 동안 2초마다 한 번씩 새 틱을 계산합니다.
- 매 틱마다 해당 시장의 주문장을 비우고 다시 만듭니다.
- 각 종목마다 가상 트레이더 주문을 새로 생성합니다.
- 생성된 주문끼리 먼저 매칭합니다.
- 그 결과로 최신 가격, 체결량, 체결 횟수, 최근 체결 기록이 갱신됩니다.
중요: 현재 구현은 "지속적으로 쌓이는 영속 주문장"보다는 "틱 단위로 재구성되는 스냅샷성 주문장"에 가깝습니다. 즉, 가상 트레이더의 미체결 지정가 주문이 여러 틱에 걸쳐 유지되는 구조가 아닙니다.
- 현재 UI에서 플레이어는
시장가방식만 사용합니다. - 플레이어 매수는 최우선 매도호가부터 순차 체결됩니다.
- 플레이어 매도는 최우선 매수호가부터 순차 체결됩니다.
- 호가 수량이 부족하면 부분 체결이 가능합니다.
- 체결 가능한 수량이 전혀 없으면 실패 메시지를 표시합니다.
- 호가창은 가격대별로 수량을 합산해 보여줍니다.
- 선택 종목 기준 매도/매수 각 6호가를 표시합니다.
- 최근 체결은 최대 8건을 표시합니다.
- 플레이어가 참여한 체결은 최근 체결 목록에서 강조 표시됩니다.
현재 메인 런타임은 js/market-simulator.js를 사용합니다. 이름은 "시장 시뮬레이터"지만 실제 역할은 아래와 같습니다.
- 100명의 가상 트레이더 생성
- 트레이더별 전략/관심시장/초기 예수금 부여
- 종목별 초기 재고 배분
- 틱마다 지정가 주문 생성
- 주문 체결 후 자산과 랭킹 갱신
| 전략 | 인원 | 설명 |
|---|---|---|
market_maker |
12명 | 양방향 호가를 제시해 유동성 공급 |
momentum |
24명 | 최근 흐름 추종 |
value |
24명 | 이동평균 대비 저평가/고평가 반응 |
contrarian |
20명 | 최근 흐름과 반대로 반응 |
swing |
20명 | 더 큰 폭의 방향성 주문 |
- 기준 자산은 1인당
1,000,000 KRW 상당입니다. - 국내 전용, 미국 전용, 양시장 관심 트레이더에 따라 KRW/USD 비중이 다르게 배분됩니다.
- 각 종목마다 마켓메이커 일부와 일반 트레이더 일부가 샘플링됩니다.
- 마켓메이커는 매수/매도 양쪽 주문을 동시에 제시합니다.
- 일반 트레이더는 모멘텀, 평균회귀, 역추세 신호를 바탕으로 한 방향 주문을 냅니다.
- 주문 가격은 시장별 호가 단위에 맞춰 반올림됩니다.
- 랭킹은 트레이더 총 자산을 KRW로 환산해 계산합니다.
- 수익률은 각 트레이더의 초기 자산 대비 현재 자산 기준입니다.
- 내부 구현은 최대 18명까지 반환할 수 있지만, 현재 UI는 상위 12명만 보여줍니다.
- 원화 예수금:
₩1,000,000 - 달러 예수금:
$0.00
- 현물 보유 종목
- 레버리지 롱 포지션
- 숏 포지션
- 원화 예수금
- 달러 예수금
총 자산은 항상 원화 기준으로 계산합니다.
- 원화 예수금은 그대로 더합니다.
- 국내 현물/포지션 가치는 원화로 합산합니다.
- 미국 현물/포지션 및 달러 예수금은 현재 환율로 원화 환산 후 합산합니다.
- 선택 종목 기준 수량 입력 후
현물 매수 - 현재 시장이 열려 있어야 함
- 해당 시장 통화 예수금이 충분해야 함
- 주문장 매도호가가 있어야 체결 가능
- 평균 체결가, 부분 체결 여부를 토스트로 표시
- 선택 종목 기준 수량 입력 후
현물 매도 - 보유 수량보다 많이 팔 수 없음
- 주문장 매수호가가 있어야 체결 가능
- 부분 체결 가능
- 1주/10주/100주 빠른 입력 버튼
최대버튼- 해당 종목 보유 수량이 있으면 보유 수량을 우선 사용
- 보유가 없으면 현재 예수금으로 살 수 있는 최대 수량을 계산
현재 UI에서 체크박스를 켜면 현물 버튼과 별개로 레버리지/숏 진입 버튼을 사용할 수 있습니다.
- 배율:
2x,3x,4x - 필요 자기자본:
총 매수금액 / 레버리지 - 수수료: 자기자본의 1%
- 차입금: 총 매수금액 - 자기자본
- 청산가: 자기자본의 90%를 잃는 가격 기준
- 증거금: 총 포지션 가치의 50%
- 수수료: 증거금의 1%
- 청산가: 증거금의 90%를 잃는 가격 기준
- 시장 틱마다 자동 청산 여부를 검사합니다.
- 청산 조건을 충족하면 포지션은 자동 제거되고, 남은 가치만 예수금으로 환원됩니다.
- 자동 청산 결과는 토스트로 알립니다.
- 레버리지 포지션 영역에서 각 포지션의 현재 가치, 손익, 청산가를 확인 가능
- 숏 포지션 영역에서 동일하게 확인 가능
- 각 포지션마다 수동 청산 버튼 제공
- 시작 환율:
1 USD = 1300 KRW - 환율 변동 주기: 5초
- 변동 폭: 대략 ±0.5%
- 환전 수수료: 1%
현재 입력 필드 환전 금액 (USD)는 양방향 모두 "달러 기준 수량"을 의미합니다.
원화 → 달러: 입력한 달러 수량을 사기 위해 필요한 원화를 차감달러 → 원화: 입력한 달러 수량만큼 팔아서 원화를 수령
- 현재 환율 표시
- 직전 환율 대비 상승/하락/보합 배지 표시
화면은 크게 세 컬럼으로 구성됩니다.
| 영역 | 역할 |
|---|---|
| 왼쪽 | 감시종목, 시장 탭, 가상 트레이더 랭킹 |
| 가운데 | 선택 종목 요약, 현재가, 요약 카드, 실시간 차트 |
| 오른쪽 | 호가/체결, 주문 티켓, 자산/포지션/환전 |
- 다크 모드 토글
- 폰트 크기 순환 변경
- 인생 리셋
- 차트 확대 모달
- 반응형 레이아웃
- 1180px 이하에서 1열 스택 구조로 전환
- 900px 이하에서 카드와 그리드가 더 단순한 1열 구조로 전환
- 640px 이하에서는 버튼과 상단 컨트롤이 모바일 레이아웃으로 재배치
- 게임 오버 오버레이 UI와
다시 시작버튼은 구현되어 있습니다. handleGameOver()보조 함수도 존재합니다.- 다만 현재
main.js의 메인 루프에서 이 함수가 실제로 호출되지 않아, 자동 게임 오버는 의도 대비 미연결 상태입니다.
- 게임 오버 모달의
다시 시작은 런타임 상태를 초기화하고 새 판을 시작합니다. - 종목, 포지션, 시장 시뮬레이터 상태를 재구성합니다.
- 우측 상단
인생 리셋은localStorage전체를 비우고 페이지를 다시 로드합니다. - 저장된 게임 상태, 시장 상태, 다크 모드, 폰트 크기 설정까지 모두 삭제됩니다.
| 키 | 역할 |
|---|---|
stockGameState |
플레이어 자산, 포지션, 시장 상태, 종목 데이터 저장 |
stockGameRealisticMarketState |
가상 트레이더 상태 저장 |
darkMode |
다크 모드 설정 저장 |
fontSizeIndex |
폰트 크기 설정 저장 |
- 페이지를 닫거나 새로고침하기 직전
beforeunload에서 저장 - 다크 모드/폰트 크기 변경 시 즉시 저장
game-state.js에는 과거 버전cash가 숫자였던 저장 데이터를 보정하는 로직이 들어 있습니다.market-simulator.js는 별도의 상태 버전(MARKET_STATE_VERSION = 2)을 사용합니다.- 저장 구조를 바꾸면 해당 버전과 로드 로직을 함께 수정하는 것이 안전합니다.
| 파일 | 역할 |
|---|---|
index.html |
실제 엔트리 HTML. 전체 레이아웃과 버튼/패널 마크업 담당 |
styles.css |
전체 UI 스타일, 다크 모드, 반응형 레이아웃 |
js/main.js |
앱 초기화, 타이머, 시장 개폐, 전역 이벤트 연결, 게임 라이프사이클 |
js/game-state.js |
플레이어 자산, 포지션, 환율, 저장/불러오기 |
js/stock.js |
종목 생성, 메타데이터, 현물/레버리지/숏 계산 |
js/market-simulator.js |
가상 트레이더, 틱 시뮬레이션, 주문 생성/체결, 랭킹 |
js/order-book.js |
주문장 자료구조, 체결 기록, 호가/체결 조회 유틸 |
js/ui.js |
DOM 렌더링, 선택 종목 패널, 포지션 목록, 랭킹, 토스트 |
js/chart.js |
Canvas 차트 및 상세 모달 렌더링 |
js/settings.js |
다크 모드/폰트 크기 설정 |
| 파일 | 상태 |
|---|---|
js/ai-trader.js |
예전 AI 트레이더 구현. 현재 main.js에서 import하지 않음 |
js/ai-trader-manager.js |
예전 AI 트레이더 풀 관리 코드. 현재 메인 런타임 미사용 |
index_old.html |
과거 단일 HTML 기반 구현 백업 |
index.html.backup |
과거 HTML 백업 |
- 모든 상태는 브라우저 메모리 + localStorage에만 존재합니다.
- API 호출이나 백엔드 동기화는 없습니다.
현재 화면 버튼 다수는 index.html에서 직접 onclick="..." 형태로 연결되어 있습니다. 그래서 js/main.js 마지막에서 여러 함수를 window.someFunction = ... 형태로 노출합니다.
새 인터랙션을 추가할 때는 두 가지 중 하나를 선택해야 합니다.
- 기존 패턴을 유지해서
window에 함수 노출 - 또는
ui.js/main.js에서 직접 이벤트 리스너를 바인딩
둘을 섞더라도 일관성을 유지하는 것이 중요합니다.
상태가 바뀌었을 때 보통 아래 조합이 호출됩니다.
renderStocks()updatePlayerInfo()refreshSelectedStockPanel()updateChartIfOpen()
새 상태 필드를 추가하면 거의 항상 game-state.js와 ui.js를 함께 수정하게 됩니다.
현재 시장은 실시간 누적 주문장이 아니라 "매 틱마다 새로 빌드하는 가상 시장"입니다. 이 구조를 바꾸면 아래 모듈이 함께 영향을 받습니다.
js/main.jsjs/market-simulator.jsjs/order-book.jsjs/ui.js
아래 둘은 항상 세트로 봐야 합니다.
saveGameState()/loadGameState()saveMarketSimulationState()/loadMarketSimulationState()
로컬 스토리지 구조를 깨면 이전 저장 데이터와 충돌할 수 있습니다.
| 작업 유형 | 우선 확인할 파일 |
|---|---|
| 새 버튼/패널 추가 | index.html, styles.css, js/ui.js, js/main.js |
| 자산 계산 규칙 변경 | js/game-state.js, js/stock.js, js/ui.js |
| 신규 주문 타입 추가 | js/order-book.js, js/market-simulator.js, js/main.js, js/ui.js |
| 시장 운영 시간 변경 | js/main.js |
| 종목 생성 규칙 변경 | js/stock.js, js/main.js |
| 랭킹 규칙 변경 | js/market-simulator.js, js/ui.js |
| 차트 표현 변경 | js/chart.js, styles.css |
| 저장 포맷 변경 | js/game-state.js, js/market-simulator.js |
- 자동화 테스트가 없습니다.
- 타입 시스템이 없습니다.
- 패키지 매니저와 빌드 단계가 없습니다.
- 플레이어 지정가 주문/예약 주문 UI는 없습니다.
- 게임 오버 자동 판정은 UI/함수는 있으나 현재 메인 루프와 연결되어 있지 않습니다.
- 레거시 파일이 저장소에 남아 있어 현재 경로와 혼동할 수 있습니다.
- 브라우저 캐시와 localStorage 때문에 "수정했는데 이상하게 예전 상태처럼 보이는" 상황이 자주 생길 수 있습니다.
- ES module 파일명이 바뀌면
index.html과 import 경로를 함께 수정해야 합니다. - 종목/포지션 구조를 바꾸면 저장된 예전 데이터와 충돌할 수 있습니다.
새로 합류한 개발자에게는 아래 순서를 권장합니다.
index.html로 화면 전체 구성을 확인합니다.js/main.js에서 앱 초기화와 틱 루프를 읽습니다.js/game-state.js와js/stock.js로 도메인 모델을 이해합니다.js/market-simulator.js로 주문장/체결/랭킹 흐름을 이해합니다.js/ui.js와js/chart.js로 렌더링 구조를 확인합니다.- 마지막으로
styles.css에서 레이아웃과 반응형 규칙을 봅니다.
이 프로젝트는 "백엔드 없는 정적 웹 트레이딩 시뮬레이터"입니다. 현재 구현의 핵심은 다음 세 가지입니다.
- 국내장/미국장을 번갈아 여는 시간 기반 게임 구조
- 100명의 가상 트레이더가 만드는 틱 기반 주문장/체결 시뮬레이션
- 현물, 레버리지, 숏, 환전, 랭킹까지 한 화면에 묶은 WTS 스타일 UI
새 기능을 추가할 때는 먼저 "이 기능이 상태 변경인지, 시뮬레이션 규칙 변경인지, 렌더링 변경인지"를 구분하면 수정 범위를 빠르게 좁힐 수 있습니다.