Stop tracking runtime and generated files

This commit is contained in:
2026-05-31 23:10:44 +09:00
parent ae72b4c739
commit ae02cdccf2
30 changed files with 42 additions and 9494 deletions
+34
View File
@@ -1,2 +1,36 @@
# Local agent/task workspace
.strideterm/
# Local secrets and environment overrides
api/config.local.php
api/config.local.php*
.env
.env.*
!.env.example
# Runtime logs
*.log
api/logs/*
!api/logs/.htaccess
# User-uploaded/generated media
uploads/*
!uploads/.htaccess
!uploads/**/.htaccess
# Local backups and temporary files
backup_*/
*_backup.*
*.bak
*.backup
*.tmp
*.temp
*.orig
*.rej
# OS/editor noise
.DS_Store
Thumbs.db
Desktop.ini
.vscode/
.idea/
+8
View File
@@ -0,0 +1,8 @@
# 로그 디렉토리 웹 직접 접근 차단
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-639
View File
@@ -1,639 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>이종재 | Game & XR Developer</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Noto+Sans+KR:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root {
--primary: #00f2ff;
--secondary: #7000ff;
--bg-dark: #0a0a0f;
--card-bg: #161625;
--text-white: #e2e8f0;
--text-dim: #94a3b8;
--danger: #ff4757;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', sans-serif;
background-color: var(--bg-dark);
color: var(--text-white);
line-height: 1.7;
overflow-x: hidden;
}
h1, h2, h3, .logo, .card-label { font-family: 'Orbitron', sans-serif; }
nav {
background: rgba(10, 10, 15, 0.95);
backdrop-filter: blur(10px);
padding: 1.2rem 8%;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 1000;
border-bottom: 1px solid rgba(0, 242, 255, 0.1);
}
nav .logo { font-weight: 700; font-size: 1.4rem; color: var(--primary); letter-spacing: 2px; }
nav .links { display: flex; align-items: center; gap: 2rem; }
nav .links a {
text-decoration: none;
color: var(--text-white);
font-size: 0.9rem;
transition: 0.3s;
display: flex;
align-items: center;
gap: 6px;
}
nav .links a:hover { color: var(--primary); }
nav .links a.profile-link {
border: 1px solid var(--primary);
padding: 0.5rem 1rem;
border-radius: 6px;
}
nav .links a.profile-link:hover {
background: var(--primary);
color: #000;
}
header {
min-height: 60vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 80px 20px;
background:
radial-gradient(circle at 30% 50%, rgba(112, 0, 255, 0.15) 0%, transparent 50%),
radial-gradient(circle at 70% 50%, rgba(0, 242, 255, 0.1) 0%, transparent 50%);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
header h1 {
font-size: 3.5rem;
margin-bottom: 1rem;
text-shadow: 0 0 30px rgba(0, 242, 255, 0.5);
letter-spacing: 2px;
}
header p { font-size: 1.2rem; color: var(--text-dim); max-width: 800px; padding: 0 20px; }
.highlight { color: var(--primary); }
main { padding: 80px 8%; }
.section-header { display: flex; align-items: center; gap: 20px; margin-bottom: 4rem; }
.section-header h2 { font-size: 2rem; letter-spacing: 1px; }
.line { flex-grow: 1; height: 1px; background: rgba(0, 242, 255, 0.2); }
.admin-controls {
display: flex;
gap: 10px;
}
.admin-controls.hidden { display: none; }
.btn {
padding: 0.6rem 1.2rem;
border-radius: 6px;
font-family: 'Orbitron', sans-serif;
font-size: 0.75rem;
letter-spacing: 1.5px;
cursor: pointer;
border: none;
transition: 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary { background: var(--primary); color: #000; font-weight: 700; }
.btn-primary:hover { background: #fff; transform: translateY(-2px); }
.btn-outline { background: transparent; border: 1px solid var(--primary); color: var(--primary); }
.btn-outline:hover { background: var(--primary); color: #000; }
.btn-danger { background: transparent; border: 1px solid var(--danger); color: var(--danger); }
.btn-danger:hover { background: var(--danger); color: #fff; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2.5rem;
}
.card {
background: var(--card-bg);
border-radius: 1.2rem;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.05);
transition: 0.3s ease;
position: relative;
}
.card:hover { transform: translateY(-10px); border-color: var(--primary); box-shadow: 0 10px 40px rgba(0, 242, 255, 0.1); }
.card-img {
width: 100%;
height: 200px;
background: linear-gradient(135deg, #1e1e30 0%, #2a1a3e 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 3.5rem;
color: rgba(255,255,255,0.15);
background-size: cover;
background-position: center;
}
.card-content { padding: 1.8rem; }
.card-label {
color: var(--primary);
font-size: 0.7rem;
letter-spacing: 2px;
margin-bottom: 0.8rem;
display: block;
font-weight: 700;
}
.card h3 { font-size: 1.4rem; margin-bottom: 0.8rem; }
.card p { color: var(--text-dim); margin-bottom: 1.5rem; font-size: 0.95rem; }
.card-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.card-actions.admin-mode {
border-top: 1px dashed rgba(255,255,255,0.1);
padding-top: 1rem;
margin-top: 1rem;
}
.btn-git {
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: #000;
background: var(--primary);
padding: 0.7rem 1.2rem;
border-radius: 0.6rem;
font-weight: 700;
font-size: 0.85rem;
transition: 0.3s;
}
.btn-git:hover { background: white; }
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-dim);
}
.empty-state i { font-size: 3rem; opacity: 0.3; margin-bottom: 1rem; }
/* 모달 */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 2000;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal-overlay.active { display: flex; }
.modal {
background: var(--card-bg);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 1rem;
padding: 2rem;
max-width: 550px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal h2 {
color: var(--primary);
margin-bottom: 1.5rem;
font-size: 1.4rem;
letter-spacing: 1px;
}
.form-group { margin-bottom: 1.2rem; }
.form-group label {
display: block;
margin-bottom: 6px;
font-size: 0.85rem;
color: var(--text-dim);
font-family: 'Orbitron', sans-serif;
letter-spacing: 1px;
}
.form-group input, .form-group textarea, .form-group select {
width: 100%;
padding: 12px;
background: #0a0a0f;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
color: var(--text-white);
font-family: 'Noto Sans KR', sans-serif;
font-size: 0.95rem;
transition: 0.3s;
}
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 242, 255, 0.1);
}
.form-group textarea { min-height: 100px; resize: vertical; }
.modal-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 1.5rem;
}
.alert {
padding: 12px;
border-radius: 6px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.alert-error { background: rgba(255, 71, 87, 0.1); border: 1px solid var(--danger); color: var(--danger); }
.alert-success { background: rgba(0, 242, 255, 0.1); border: 1px solid var(--primary); color: var(--primary); }
.icon-hint {
font-size: 0.75rem;
color: var(--text-dim);
margin-top: 4px;
}
.icon-hint a { color: var(--primary); }
footer {
padding: 60px 8% 30px;
text-align: center;
border-top: 1px solid rgba(255,255,255,0.05);
color: var(--text-dim);
font-size: 0.9rem;
}
footer .admin-toggle {
display: inline-block;
margin-top: 10px;
opacity: 0.3;
cursor: pointer;
transition: 0.3s;
}
footer .admin-toggle:hover { opacity: 1; color: var(--primary); }
@media (max-width: 768px) {
header h1 { font-size: 2.2rem; }
.grid { grid-template-columns: 1fr; }
nav { padding: 1rem 5%; }
nav .links { gap: 1rem; }
main { padding: 60px 5%; }
}
</style>
</head>
<body>
<nav>
<div class="logo">JONGJAE.XR</div>
<div class="links">
<a href="#work">PROJECTS</a>
<a href="profile.html" class="profile-link">
<i class="fa-solid fa-user"></i> PROFILE
</a>
</div>
</nav>
<header>
<h1>이종재 <span class="highlight">Portfolio</span></h1>
<p>Game & XR 개발자로서의 기술적 도전과 기록을 담은 공간입니다.<br>Gitea 서버를 통해 실제 <span class="highlight">소스 코드</span>를 확인하실 수 있습니다.</p>
</header>
<main id="work">
<div class="section-header">
<h2>DEVELOPMENT LOG</h2>
<div class="line"></div>
<div class="admin-controls hidden" id="adminControls">
<button class="btn btn-primary" onclick="openProjectModal()">
<i class="fa-solid fa-plus"></i> 새 프로젝트
</button>
<button class="btn btn-outline" onclick="logout()">
<i class="fa-solid fa-right-from-bracket"></i> 로그아웃
</button>
</div>
</div>
<div class="grid" id="projectsGrid">
<!-- 프로젝트가 동적으로 로드됩니다 -->
</div>
</main>
<footer>
<p>&copy; 2026 Lee Jong-jae. Hosted on Private Synology NAS.</p>
<span class="admin-toggle" onclick="openLoginModal()" title="Admin">
<i class="fa-solid fa-shield-halved"></i>
</span>
</footer>
<!-- 로그인 모달 -->
<div class="modal-overlay" id="loginModal">
<div class="modal">
<h2><i class="fa-solid fa-lock"></i> ADMIN LOGIN</h2>
<div id="loginAlert"></div>
<div class="form-group">
<label>비밀번호</label>
<input type="password" id="loginPassword" placeholder="비밀번호를 입력하세요"
onkeypress="if(event.key==='Enter') doLogin()">
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeLoginModal()">취소</button>
<button class="btn btn-primary" onclick="doLogin()">로그인</button>
</div>
</div>
</div>
<!-- 프로젝트 등록/수정 모달 -->
<div class="modal-overlay" id="projectModal">
<div class="modal">
<h2 id="projectModalTitle">새 프로젝트</h2>
<div id="projectAlert"></div>
<input type="hidden" id="projectId">
<div class="form-group">
<label>제목 *</label>
<input type="text" id="projectTitle" placeholder="예: XR 인터랙티브 시뮬레이션">
</div>
<div class="form-group">
<label>라벨 *</label>
<input type="text" id="projectLabel" placeholder="예: UNITY / XR">
</div>
<div class="form-group">
<label>설명 *</label>
<textarea id="projectDescription" placeholder="프로젝트 소개"></textarea>
</div>
<div class="form-group">
<label>아이콘 (Font Awesome 클래스)</label>
<input type="text" id="projectIcon" placeholder="예: fa-solid fa-cube">
<p class="icon-hint">예시: <code>fa-solid fa-cube</code> · <code>fa-solid fa-gamepad</code> · <code>fa-solid fa-microchip</code> · <a href="https://fontawesome.com/search?o=r&m=free" target="_blank">아이콘 검색</a></p>
</div>
<div class="form-group">
<label>썸네일 이미지 URL (선택)</label>
<input type="text" id="projectImage" placeholder="비우면 아이콘 표시">
</div>
<div class="form-group">
<label>링크 URL (Gitea 저장소 등)</label>
<input type="text" id="projectLink" placeholder="https://...">
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeProjectModal()">취소</button>
<button class="btn btn-primary" onclick="saveProject()">
<i class="fa-solid fa-floppy-disk"></i> 저장
</button>
</div>
</div>
</div>
<script>
// ===== 상태 =====
let isAdmin = false;
let projects = [];
// ===== 초기화 =====
async function init() {
await checkAuth();
await loadProjects();
}
// ===== 인증 =====
async function checkAuth() {
try {
const res = await fetch('api/auth.php?action=check');
const data = await res.json();
isAdmin = data.authenticated === true;
document.getElementById('adminControls').classList.toggle('hidden', !isAdmin);
} catch (e) {
isAdmin = false;
}
}
function openLoginModal() {
if (isAdmin) {
logout();
return;
}
document.getElementById('loginModal').classList.add('active');
document.getElementById('loginPassword').focus();
}
function closeLoginModal() {
document.getElementById('loginModal').classList.remove('active');
document.getElementById('loginPassword').value = '';
document.getElementById('loginAlert').innerHTML = '';
}
async function doLogin() {
const password = document.getElementById('loginPassword').value;
if (!password) return;
try {
const res = await fetch('api/auth.php?action=login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
});
const data = await res.json();
if (data.success) {
isAdmin = true;
document.getElementById('adminControls').classList.remove('hidden');
closeLoginModal();
await loadProjects();
} else {
showAlert('loginAlert', data.error || '로그인 실패', 'error');
}
} catch (e) {
showAlert('loginAlert', '서버 오류', 'error');
}
}
async function logout() {
await fetch('api/auth.php?action=logout', { method: 'POST' });
isAdmin = false;
document.getElementById('adminControls').classList.add('hidden');
await loadProjects();
}
// ===== 프로젝트 로드 =====
async function loadProjects() {
try {
const res = await fetch('api/projects.php');
projects = await res.json();
renderProjects();
} catch (e) {
console.error('Failed to load projects', e);
}
}
function renderProjects() {
const grid = document.getElementById('projectsGrid');
if (!projects || projects.length === 0) {
grid.innerHTML = `
<div class="empty-state" style="grid-column: 1/-1;">
<i class="fa-solid fa-folder-open"></i>
<p>아직 등록된 프로젝트가 없습니다.</p>
</div>
`;
return;
}
grid.innerHTML = projects.map(p => `
<div class="card">
<div class="card-img" ${p.image ? `style="background-image:url('${escapeHtml(p.image)}'); font-size:0;"` : ''}>
${!p.image ? `<i class="${escapeHtml(p.icon || 'fa-solid fa-code')}"></i>` : ''}
</div>
<div class="card-content">
<span class="card-label">${escapeHtml(p.label)}</span>
<h3>${escapeHtml(p.title)}</h3>
<p>${escapeHtml(p.description)}</p>
<div class="card-actions">
${p.link ? `<a href="${escapeHtml(p.link)}" class="btn-git" target="_blank">
<i class="fa-brands fa-git-alt"></i> 소스 코드 보기
</a>` : ''}
</div>
${isAdmin ? `
<div class="card-actions admin-mode">
<button class="btn btn-outline" onclick="editProject(${p.id})">
<i class="fa-solid fa-pen"></i> 수정
</button>
<button class="btn btn-danger" onclick="deleteProject(${p.id})">
<i class="fa-solid fa-trash"></i> 삭제
</button>
</div>` : ''}
</div>
</div>
`).join('');
}
// ===== 프로젝트 모달 =====
function openProjectModal(project = null) {
document.getElementById('projectAlert').innerHTML = '';
if (project) {
document.getElementById('projectModalTitle').textContent = '프로젝트 수정';
document.getElementById('projectId').value = project.id;
document.getElementById('projectTitle').value = project.title;
document.getElementById('projectLabel').value = project.label;
document.getElementById('projectDescription').value = project.description;
document.getElementById('projectIcon').value = project.icon || '';
document.getElementById('projectImage').value = project.image || '';
document.getElementById('projectLink').value = project.link || '';
} else {
document.getElementById('projectModalTitle').textContent = '새 프로젝트';
document.getElementById('projectId').value = '';
['projectTitle', 'projectLabel', 'projectDescription',
'projectIcon', 'projectImage', 'projectLink'].forEach(id => {
document.getElementById(id).value = '';
});
document.getElementById('projectIcon').value = 'fa-solid fa-code';
}
document.getElementById('projectModal').classList.add('active');
}
function closeProjectModal() {
document.getElementById('projectModal').classList.remove('active');
}
function editProject(id) {
const project = projects.find(p => p.id === id);
if (project) openProjectModal(project);
}
async function saveProject() {
const id = document.getElementById('projectId').value;
const data = {
title: document.getElementById('projectTitle').value.trim(),
label: document.getElementById('projectLabel').value.trim(),
description: document.getElementById('projectDescription').value.trim(),
icon: document.getElementById('projectIcon').value.trim(),
image: document.getElementById('projectImage').value.trim(),
link: document.getElementById('projectLink').value.trim()
};
if (!data.title || !data.label || !data.description) {
showAlert('projectAlert', '제목, 라벨, 설명은 필수입니다', 'error');
return;
}
try {
let res;
if (id) {
data.id = parseInt(id);
res = await fetch('api/projects.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} else {
res = await fetch('api/projects.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
const result = await res.json();
if (result.success) {
closeProjectModal();
await loadProjects();
} else {
showAlert('projectAlert', result.error || '저장 실패', 'error');
}
} catch (e) {
showAlert('projectAlert', '서버 오류', 'error');
}
}
async function deleteProject(id) {
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const res = await fetch(`api/projects.php?id=${id}`, { method: 'DELETE' });
const result = await res.json();
if (result.success) {
await loadProjects();
} else {
alert(result.error || '삭제 실패');
}
} catch (e) {
alert('서버 오류');
}
}
// ===== 유틸 =====
function showAlert(elemId, message, type) {
const elem = document.getElementById(elemId);
elem.innerHTML = `<div class="alert alert-${type}">${escapeHtml(message)}</div>`;
if (type === 'success') {
setTimeout(() => elem.innerHTML = '', 3000);
}
}
function escapeHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// 모달 외부 클릭 시 닫기
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.classList.remove('active');
});
});
init();
</script>
</body>
</html>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 927 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1019 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
@@ -1,536 +0,0 @@
# 단타 자동매매 시스템 종합 기획서
> 버전: v1.0
> 기준: 한국 주식 (코스피/코스닥) / KIS Open API / Synology NAS Docker
> 원칙: 규칙 기반 완전 자동화, 인간 판단 개입 0
---
## 0. 설계 원칙 (절대 불변)
1. **감정 0** — 모든 진입/청산은 코드가 결정. 예외 없음
2. **손절 우선** — 수익 극대화보다 손실 제한이 먼저
3. **단순함 우선** — 복잡한 전략보다 단순하고 견고한 전략
4. **검증 후 실거래** — 백테스트 → 모의투자 3개월 → 소액 실거래 순서 필수
5. **14:50 전량 청산** — 오버나이트는 절대 없음. 하드코딩
---
## 1. 시스템 전체 구조
```
┌─────────────────────────────────────────────────────────┐
│ Synology NAS (Docker) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ stockbot │ │ kill-switch │ ← 별도 컨테이너 │
│ │ (메인) │ │ (긴급중단) │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ┌──────▼───────────────────────────────────┐ │
│ │ asyncio Event Loop │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────────┐ │ │
│ │ │Universe │ │Strategy │ │ Risk │ │ │
│ │ │Scanner │ │Engine │ │ Manager │ │ │
│ │ └─────────┘ └─────────┘ └───────────┘ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────────┐ │ │
│ │ │ Data │ │ Order │ │ Notifier │ │ │
│ │ │Collector│ │Executor │ │(Telegram) │ │ │
│ │ └─────────┘ └─────────┘ └───────────┘ │ │
│ └───────────────────────┬──────────────────┘ │
│ │ │
│ ┌───────────────────────▼──────────────────┐ │
│ │ SQLite (체결/포지션/로그) │ │
│ │ Redis (실시간 시세 캐시) │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Streamlit Dashboard (포트 8501) │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │
KIS WebSocket KIS REST API
(실시간 시세) (주문/잔고)
```
---
## 2. 기술 스택 (확정)
| 항목 | 선택 | 이유 |
|------|------|------|
| 언어 | Python 3.11 | 생태계, KIS 예제 코드 모두 Python |
| 비동기 | asyncio + aiohttp | 초당 20건 rate limit 정밀 제어 |
| DB | SQLite | NAS 메모리 절약, 단일 파일 백업 용이 |
| 캐시 | Redis 7 (Docker) | 실시간 시세 캐시, TTL 관리 |
| 스케줄러 | APScheduler 3.x (AsyncIOScheduler) | 장 시작/마감 이벤트 트리거 |
| 알림 | python-telegram-bot | 단일 채널, 신뢰성 높음 |
| 대시보드 | Streamlit | 설치 단순, NAS 내부망 접근 |
| 컨테이너 | Docker Compose | Synology Container Manager 호환 |
| 백테스트 | vectorbt | pandas 기반, 분봉 데이터 처리 빠름 |
| 데이터 수집 | pykrx + KIS REST | 과거 분봉 백필 |
---
## 3. 디렉토리 구조
```
/volume1/docker/stockbot/
├── docker-compose.yml
├── .env ← API 키 전용 (Git 제외)
├── app/
│ ├── main.py ← 진입점, asyncio 루프
│ ├── config.py ← 전략 파라미터 전용
│ │
│ ├── data/
│ │ ├── collector.py ← KIS WebSocket 시세 수신
│ │ ├── universe.py ← 종목 풀 갱신 (08:30)
│ │ └── backfill.py ← 과거 분봉 백필 스크립트
│ │
│ ├── strategy/
│ │ ├── base.py ← 전략 추상 클래스
│ │ └── volatility_breakout.py ← 전략 A (유일한 실전 전략)
│ │
│ ├── risk/
│ │ └── manager.py ← 손절/일일한도/강제청산
│ │
│ ├── execution/
│ │ ├── kis_client.py ← KIS REST/WebSocket 래퍼
│ │ └── order_executor.py ← 주문 전송, 재시도, 슬리피지
│ │
│ ├── monitor/
│ │ ├── notifier.py ← 텔레그램 알림
│ │ └── dashboard.py ← Streamlit 대시보드
│ │
│ └── db/
│ ├── models.py ← SQLite 스키마
│ └── repository.py ← DB 접근 레이어
├── kill_switch/
│ └── kill.py ← 긴급 전량 청산 단독 실행
├── backtest/
│ ├── run_backtest.py ← vectorbt 백테스트 실행
│ └── results/ ← 백테스트 결과 저장
├── data/
│ ├── stockbot.db ← SQLite DB
│ └── universe_cache.json ← 당일 종목 풀 캐시
└── logs/
└── trades.log ← 영구 보관 (세금 신고용)
```
---
## 4. 전략 (확정: 전략 A 단독)
### 변동성 돌파 전략 (Volatility Breakout)
**선정 이유:**
- 래리 윌리엄스 실전 검증, 30년 이상 데이터 기반
- 룰이 단순 → 과적합 위험 최소
- 분봉 적용 시 한국 장 오전 변동성과 궁합 최적
- 진입 조건이 명확 → 자동화 구현 난이도 낮음
**핵심 파라미터 (config.py에서 관리)**
```python
STRATEGY_K = 0.5 # 변동성 계수 (0.4~0.6 안정 구간)
ENTRY_START = "09:00" # 진입 허용 시작
ENTRY_END = "14:30" # 진입 허용 마감
FORCE_EXIT = "14:50" # 강제 청산 시각 (하드코딩)
TP1_PCT = 0.02 # 1차 익절 +2% (50% 매도)
TP2_PCT = 0.03 # 2차 익절 +3% (전량)
SL_PCT = 0.015 # 손절 -1.5% (즉시 시장가)
MAX_HOLD_MIN = 120 # 최대 보유시간 120분 (무수익 시 청산)
```
**진입 로직**
```
목표가 = 당일 시가 + (전일 고가 - 전일 저가) × K
진입 조건 (전부 충족 시):
1. 현재가 >= 목표가
2. 현재 시각 ENTRY_START ~ ENTRY_END
3. 당일 KOSPI 등락률 > -1.0% ← 시장 약세 차단
4. 전일 거래대금 >= 100억
5. 시가총액 1,000억 ~ 3조
6. VI 미발동 종목
7. 당일 보유 종목 수 < 2 ← 동시 최대 2종목
8. 일일 누적 손실 < -3% ← 손실 한도 미도달
```
**청산 로직 (우선순위 순)**
```
1순위: 강제 청산 → 14:50 시장가 전량
2순위: 손절 → 현재가 <= 매수가 × (1 - SL_PCT) → 시장가
3순위: 1차 익절 → 현재가 >= 매수가 × (1 + TP1_PCT) → 50% 지정가
4순위: 2차 익절 → 현재가 >= 매수가 × (1 + TP2_PCT) → 전량 지정가
5순위: 시간 청산 → 보유 후 MAX_HOLD_MIN 경과, 수익 없을 시 시장가
```
---
## 5. 종목 유니버스 선정 (Universe)
매일 08:30 자동 갱신.
**1차 필터 (정적)**
| 조건 | 기준 |
|------|------|
| 시장 | 코스피 + 코스닥 |
| 시가총액 | 1,000억 ~ 3조 |
| 상장 기간 | 6개월 이상 |
| 제외 | 관리종목, 거래정지, 우선주, 스팩, ETF, ETN |
**2차 필터 (동적, 전일 기준)**
| 조건 | 기준 |
|------|------|
| 전일 거래대금 | 100억 이상 |
| 5일 평균 거래대금 | 50억 이상 |
| 전일 등락률 | -3% ~ +15% |
| 일봉 60일 이평선 | 현재가 위 |
**결과: 최대 30종목** (KIS WebSocket 안정성 기준으로 50 → 30 축소)
---
## 6. 리스크 관리
### 포지션 규칙
| 항목 | 값 |
|------|-----|
| 1종목 최대 비중 | 총자산 × 20% |
| 동시 보유 최대 | 2종목 |
| 분할매수 | 1차 10%, 신호 유지 시 2차 10% |
### 손실 한도 (계층별 자동 중단)
| 레벨 | 조건 | 동작 |
|------|------|------|
| L1 | 1회 매매 -1.5% | 즉시 손절 |
| L2 | 일일 누적 -3% | 당일 신규 진입 중단 |
| L3 | 3연속 손절 | 당일 매매 중단 |
| L4 | 주간 누적 -7% | 주말까지 중단 + 텔레그램 경고 |
| L5 | 월간 누적 -15% | 자동 전략 폐기 + 백테스트 재요청 |
### 안전장치
- VI(변동성완화장치) 발동 → 즉시 해당 종목 청산
- 호가 스프레드 > 0.5% → 진입 금지
- 매수 후 5분 미체결 → 주문 취소
- WebSocket 끊김 → 보유 포지션 즉시 시장가 청산
- API 오류 10건/분 → kill-switch 컨테이너 자동 실행
---
## 7. KIS API 운용
### 초당 20건 제한 대응 전략
```
WebSocket (실시간 시세) : 30종목 구독 → REST 호출 없음
REST (주문/잔고) : 주문 시에만 호출
REST (분봉 조회) : 신호 확인용, 1초 간격 제한
asyncio Semaphore(20) 로 초당 20건 보장
배치 처리: 30종목 ÷ 20건 = 1.5초/1회전
```
### WebSocket 구독 항목
- 실시간 체결가 (H0STCNT0)
- 실시간 호가 (H0STASP0)
- VI 발동/해제 (H0STVI0)
### 주문 타입 결정
| 상황 | 주문 타입 |
|------|----------|
| 진입 | 시장가 (빠른 체결 우선) |
| 1차 익절 | 지정가 (목표가 미리 걸기) |
| 2차 익절 | 지정가 |
| 손절 | 시장가 (슬리피지 감수) |
| 강제청산 | 시장가 |
---
## 8. 스케줄 타임라인
```
08:00 │ 시스템 기동, DB 연결 확인
08:30 │ Universe 갱신 (pykrx 전일 데이터)
08:50 │ WebSocket 연결, 30종목 구독 시작
│ 당일 시가 기반 목표가 계산
09:00 │ ─────────── 진입 허용 시작 ───────────
│ 1분봉 루프 시작 (asyncio, 1초 단위)
11:30 │ 신규 진입 일시 중단 (점심)
13:00 │ 신규 진입 재개
14:30 │ 신규 진입 마감
14:50 │ ─────────── 강제 청산 ───────────────
│ 보유 포지션 전량 시장가 매도
15:00 │ WebSocket 연결 종료
15:10 │ 당일 결산 로그 생성
15:20 │ 텔레그램 일일 결산 알림 발송
15:30 │ 시스템 대기 (다음 날 08:00까지)
```
---
## 9. DB 스키마 (SQLite)
```sql
-- 체결 내역
CREATE TABLE trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL, -- YYYY-MM-DD
ticker TEXT NOT NULL,
entry_time TEXT NOT NULL, -- HH:MM:SS
exit_time TEXT,
entry_price REAL NOT NULL,
exit_price REAL,
quantity INTEGER NOT NULL,
side TEXT NOT NULL, -- BUY / SELL
exit_reason TEXT, -- TP1/TP2/SL/FORCE/TIME
pnl REAL,
fee REAL,
slippage REAL,
strategy TEXT DEFAULT 'VB'
);
-- 일일 요약
CREATE TABLE daily_summary (
date TEXT PRIMARY KEY,
total_trades INTEGER,
win_trades INTEGER,
lose_trades INTEGER,
gross_pnl REAL,
total_fee REAL,
net_pnl REAL,
max_drawdown REAL,
trading_stopped INTEGER DEFAULT 0 -- L2 발동 여부
);
-- 포지션 (장중 현황)
CREATE TABLE positions (
ticker TEXT PRIMARY KEY,
entry_time TEXT,
entry_price REAL,
quantity INTEGER,
tp1_done INTEGER DEFAULT 0,
target_price REAL,
stop_price REAL
);
```
---
## 10. 알림 설계 (텔레그램)
| 이벤트 | 메시지 형식 |
|--------|-----------|
| 진입 | `[매수] 삼성전자 74,000원 / 목표 75,480 / 손절 72,890` |
| 1차 익절 | `[익절1] 삼성전자 +2.1% / 잔여 50%` |
| 손절 | `[손절] 삼성전자 -1.5% / 즉시 청산` |
| 강제 청산 | `[14:50 강제청산] 전 포지션 청산 완료` |
| L2 발동 | `[경고] 일일 손실 -3% 도달. 오늘 매매 중단.` |
| 일일 결산 | `[결산] 매매 5회 / 승 3 패 2 / 순손익 +1.2%` |
| 장애 | `[긴급] WebSocket 끊김. kill-switch 실행.` |
---
## 11. 백테스트 계획
### 데이터 준비
```
수집 대상: 코스피200 + 코스닥150 편입 종목
기간: 2021-01-01 ~ 2024-12-31 (4년치)
봉: 1분봉
도구: pykrx (무료, 과거 분봉 제공)
생존편향: 상장폐지 종목 별도 수집 포함
```
### 수수료 시뮬레이션
```
매수 수수료: 0.015%
매도 수수료: 0.015%
거래세: 0.18% (매도 시)
슬리피지: 시장가 0.10%, 지정가 0.03%
총 1회전 비용 추정: 약 0.21%
```
### 합격 기준 (out-of-sample 1년 기준)
| 지표 | 통과 기준 | 목표 |
|------|----------|------|
| 샤프지수 | > 1.0 | > 1.5 |
| MDD | < 15% | < 10% |
| 승률 | > 45% | > 55% |
| 손익비 | > 1.3 | > 1.8 |
| 일평균 매매 | 1~5회 | 2~3회 |
### K값 최적화 방법
```
1. K = 0.3 ~ 0.8 구간 0.05 단위 그리드 서치
2. 인샘플(2021~2023) 에서 상위 3개 K값 선별
3. 아웃샘플(2024) 에서 재검증
4. 피크 K값 배제 → 샤프지수 안정 구간 채택
5. 최종 채택: K = 0.5 (사전 기본값, 백테스트로 재확인)
```
---
## 12. 인프라 (Synology Docker)
### docker-compose.yml
```yaml
version: "3.9"
services:
redis:
image: redis:7-alpine
container_name: stockbot-redis
restart: unless-stopped
volumes:
- ./data/redis:/data
stockbot:
build: ./app
container_name: stockbot-main
restart: unless-stopped
depends_on:
- redis
env_file: .env
volumes:
- ./data:/app/data
- ./logs:/app/logs
environment:
- TZ=Asia/Seoul
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
kill-switch:
build: ./kill_switch
container_name: stockbot-killswitch
restart: "no" # 수동 또는 자동 트리거 시에만 실행
env_file: .env
profiles: ["emergency"] # docker compose --profile emergency up
dashboard:
build: ./monitor
container_name: stockbot-dashboard
restart: unless-stopped
ports:
- "8501:8501"
volumes:
- ./data:/app/data
environment:
- TZ=Asia/Seoul
```
### .env 구조
```
# KIS API
KIS_APP_KEY=...
KIS_APP_SECRET=...
KIS_ACCOUNT_NO=...
KIS_MOCK=true # 모의투자: true / 실거래: false
# 텔레그램
TELEGRAM_TOKEN=...
TELEGRAM_CHAT_ID=...
# Redis
REDIS_HOST=stockbot-redis
REDIS_PORT=6379
# 운영 모드
DRY_RUN=true # 주문 실제 전송 여부
LOG_LEVEL=INFO
```
---
## 13. 개발 로드맵
### Phase 1 — 데이터 기반 구축 (2주)
- [ ] KIS Open API 신청 + 모의투자 계좌 개설
- [ ] pykrx로 2021~2024 분봉 데이터 백필
- [ ] SQLite 스키마 생성
- [ ] Universe 스캐너 구현 (08:30 갱신)
- [ ] Docker Compose 기동 확인 (NAS)
### Phase 2 — 백테스트 (3주)
- [ ] vectorbt 환경 구성
- [ ] 변동성 돌파 전략 백테스트 코드 작성
- [ ] 수수료/슬리피지 반영
- [ ] K값 그리드 서치
- [ ] out-of-sample 검증 → 합격 기준 통과 확인
### Phase 3 — KIS 연동 + dry-run (2주)
- [ ] KIS REST 클라이언트 구현 (인증/토큰 갱신)
- [ ] KIS WebSocket 클라이언트 구현 (시세 수신)
- [ ] OrderExecutor 구현 (시장가/지정가/IOC)
- [ ] DRY_RUN=true 상태에서 신호 발생 확인
### Phase 4 — 리스크 + 알림 + 대시보드 (2주)
- [ ] RiskManager 전 계층 구현 (L1~L5)
- [ ] kill-switch 컨테이너 구현
- [ ] 텔레그램 Notifier 구현
- [ ] Streamlit 대시보드 구현 (실시간 PnL)
### Phase 5 — 모의투자 실운영 (최소 3개월)
- [ ] KIS_MOCK=true, DRY_RUN=false
- [ ] 매일 결산 로그 검토
- [ ] 주간 샤프지수/MDD 추적
- [ ] 이상 거동 발생 시 전략 파라미터 재검토 (코드 수정, K값 조정)
- [ ] 3개월 연속 샤프 > 1.0, MDD < 15% 달성 시 Phase 6 진입
### Phase 6 — 소액 실거래 (무기한)
- [ ] KIS_MOCK=false
- [ ] 총자산의 5% 한도로 시작
- [ ] 1개월 단위 성과 검토, 한도 점진 확대
---
## 14. 보안 체크리스트
- [ ] .env → .gitignore 등록 필수
- [ ] KIS API 키 → GitHub 절대 커밋 금지
- [ ] Synology 방화벽: 8501 포트 내부망만 허용
- [ ] 텔레그램 봇: 특정 chat_id만 허용 (화이트리스트)
- [ ] 모든 주문 로그 SQLite + logs/ 이중 보관
- [ ] 월 1회 .env 파일 암호화 백업
---
## 15. 절대 금지 항목 (하드코딩)
```python
# 이 리스트는 코드에서 상수로 관리, 절대 런타임 수정 불가
BLACKLIST_REASONS = [
"신규상장 6개월 미만",
"관리종목",
"투자경고",
"거래정지",
"우선주",
"스팩",
"ETF/ETN",
]
TRADING_BLACKOUT = [
("11:30", "13:00"), # 점심 휴식
("14:50", "15:30"), # 장 마감 전
("08:00", "09:00"), # 동시호가
]
HARD_EXIT_TIME = "14:50" # 절대 변경 불가
```
---
## 16. 면책 조항
> 본 기획서는 시스템 설계 문서이며, 투자 수익을 보장하지 않는다.
> 단타는 통계적으로 개인투자자의 90% 이상이 손실을 보는 영역이다.
> 백테스트 결과는 미래 수익을 보장하지 않으며, 반드시 모의투자 3개월 이상 검증 후 실거래로 전환할 것.
> 시스템 장애, API 오류, 네트워크 단절로 인한 손실에 대해 어떠한 책임도 지지 않는다.
@@ -1,743 +0,0 @@
# 단타 자동매매 시스템 종합 기획서 v2.0
> 버전: v2.0 (AI 판단 레이어 통합)
> 기준: 한국 주식 (코스피/코스닥) / KIS Open API / Synology NAS Docker
> 핵심 변경: 규칙 기반 실행 + Claude AI 일일 시장 판단 레이어 추가
---
## 0. 설계 원칙 (절대 불변)
1. **감정 0** — 실행은 코드가 결정. 단, 판단은 AI가 보조
2. **속도/판단 역할 분리** — AI는 느리지만 깊게(매일 1회), 실행은 빠르게(수식)
3. **손절 우선** — AI가 긍정 판단해도 손절 룰은 무조건 우선
4. **검증 후 실거래** — 백테스트 → 모의투자 3개월 → 소액 실거래
5. **14:50 전량 청산** — 하드코딩, 어떤 상황에서도 예외 없음
---
## 1. 시스템 전체 구조 (v2.0)
```
┌──────────────────────────────────────────────────────────────┐
│ Synology NAS (Docker) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [레이어 1] AI 판단 엔진 (08:00) │ │
│ │ │ │
│ │ 데이터 수집 │ │
│ │ ├─ 전일 뉴스 (네이버 금융 크롤링) │ │
│ │ ├─ KOSPI/KOSDAQ 지수 흐름 (KIS REST) │ │
│ │ ├─ 외국인/기관 순매수 상위 (KIS 순위분석 API) │ │
│ │ ├─ 거래량 급증 종목 (KIS 순위분석 API) │ │
│ │ └─ 섹터별 등락률 (KIS 업종/기타 API) │ │
│ │ ↓ │ │
│ │ Claude API 호출 (하루 1회, 약 3,000 토큰) │ │
│ │ ↓ │ │
│ │ daily_context.json 생성 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ 읽기만 함 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [레이어 2] 규칙 기반 실행 엔진 (09:00~) │ │
│ │ │ │
│ │ asyncio Event Loop │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ │
│ │ │Universe │ │Strategy │ │ Risk │ │ │
│ │ │Scanner │ │Engine │ │ Manager │ │ │
│ │ └──────────┘ └──────────┘ └────────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ │
│ │ │ Data │ │ Order │ │ Notifier │ │ │
│ │ │Collector │ │Executor │ │(Telegram) │ │ │
│ │ └──────────┘ └──────────┘ └────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SQLite (체결/포지션/로그) │ Redis (실시간 시세 캐시) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Streamlit Dashboard (포트 8501) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ kill-switch (별도 컨테이너 / 긴급 청산) │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ │
KIS WebSocket KIS REST API
(실시간 시세/VI) (주문/잔고/순위/수급)
```
---
## 2. 핵심 변경: AI 판단 레이어 상세
### 2-1. 수집 데이터 소스
| 데이터 | 소스 | 방법 | 수집 시각 |
|--------|------|------|---------|
| 전일 종목 뉴스 헤드라인 | 네이버 금융 | HTTP 크롤링 | 07:30 |
| KOSPI/KOSDAQ 전일 종가·등락률 | KIS REST | API | 07:40 |
| 외국인 순매수 상위 30종목 | KIS 순위분석 API | API | 07:40 |
| 기관 순매수 상위 30종목 | KIS 순위분석 API | API | 07:40 |
| 거래량 급증 상위 30종목 | KIS 순위분석 API | API | 07:40 |
| 업종별 등락률 | KIS 업종/기타 API | API | 07:40 |
| 당일 경제 일정 (FOMC 등) | 네이버 금융 크롤링 | HTTP 크롤링 | 07:30 |
> KIS API만으로 수급·수치 데이터 대부분 커버 가능. 뉴스는 네이버 금융 크롤링으로 보완.
### 2-2. Claude API 프롬프트 구조
```python
SYSTEM_PROMPT = """
당신은 한국 주식 단타 전문 AI 분석가입니다.
매일 장 시작 전, 제공된 데이터를 분석해 오늘 단타 매매 전략을 판단합니다.
반드시 JSON 형식으로만 응답하며, 다른 텍스트는 포함하지 않습니다.
"""
USER_PROMPT = f"""
[전일 시장 요약]
- KOSPI: {kospi_change}% ({kospi_close}pt)
- KOSDAQ: {kosdaq_change}% ({kosdaq_close}pt)
[외국인 순매수 상위]
{foreign_buy_top10}
[기관 순매수 상위]
{institution_buy_top10}
[거래량 급증 상위]
{volume_surge_top10}
[업종별 등락률]
{sector_changes}
[주요 뉴스 헤드라인 (상위 20건)]
{news_headlines}
[오늘 경제 일정]
{economic_calendar}
위 데이터를 분석해 다음 JSON을 반환하세요:
{{
"trade_allowed": true/false,
"market_sentiment": "강세/중립/약세",
"sentiment_score": 0~100,
"risk_level": "낮음/보통/높음",
"hot_sectors": ["섹터1", "섹터2"],
"avoid_sectors": ["섹터3"],
"boosted_tickers": ["005930", "000660"],
"blacklist_tickers": ["종목코드"],
"position_size_multiplier": 0.5~1.5,
"reason": "한 줄 판단 이유"
}}
"""
```
### 2-3. daily_context.json 구조
```json
{
"date": "2026-05-13",
"generated_at": "08:05:22",
"trade_allowed": true,
"market_sentiment": "중립",
"sentiment_score": 62,
"risk_level": "보통",
"hot_sectors": ["반도체", "2차전지"],
"avoid_sectors": ["금융", "건설"],
"boosted_tickers": ["005930", "000660", "373220"],
"blacklist_tickers": [],
"position_size_multiplier": 1.0,
"reason": "외국인 반도체 순매수 지속, KOSPI 박스권 상단 접근 중립 판단"
}
```
### 2-4. 실행 엔진에서의 활용
```python
# 진입 조건에 AI 판단 필터 추가 (기존 조건 1~8 + 신규 9~11)
진입 조건 (전부 충족 시):
1. 현재가 >= 목표가 (변동성 돌파)
2. 현재 시각 09:00 ~ 14:30
3. KOSPI 등락률 > -1.0%
4. 전일 거래대금 >= 100억
5. 시가총액 1,000억 ~ 3조
6. VI 미발동
7. 보유 종목 수 < 2
8. 일일 누적 손실 < -3%
── AI 판단 필터 (신규) ──
9. daily_context["trade_allowed"] == true
10. 해당 종목 섹터가 avoid_sectors에 없음
11. 해당 종목이 blacklist_tickers에 없음
# 포지션 사이즈 조정
실제_투자비중 = 기본비중(20%) × position_size_multiplier
# sentiment_score 높을수록 multiplier 증가 (0.5~1.5)
# boosted_tickers 우선 진입
boosted 종목은 동일 신호 시 다른 종목보다 먼저 진입 처리
```
---
## 3. 기술 스택 (v2.0 확정)
| 항목 | 선택 | 이유 |
|------|------|------|
| 언어 | Python 3.11 | KIS 예제 코드 모두 Python |
| 비동기 | asyncio + aiohttp | rate limit 정밀 제어 |
| DB | SQLite | NAS 메모리 절약, 백업 단순 |
| 캐시 | Redis 7 (Docker) | 실시간 시세 캐시 |
| 스케줄러 | APScheduler (AsyncIOScheduler) | 장 이벤트 트리거 |
| AI 판단 | Claude claude-sonnet-4-20250514 | 뉴스/수급 분석 |
| 뉴스 수집 | aiohttp + BeautifulSoup4 | 네이버 금융 크롤링 |
| 알림 | python-telegram-bot | 텔레그램 단일 채널 |
| 대시보드 | Streamlit | NAS 내부망 접근 |
| 컨테이너 | Docker Compose | Synology Container Manager |
| 백테스트 | vectorbt | 분봉 데이터 고속 처리 |
| 데이터 수집 | pykrx + KIS REST | 과거 분봉 백필 |
---
## 4. 디렉토리 구조 (v2.0)
```
/volume1/docker/stockbot/
├── docker-compose.yml
├── .env
├── app/
│ ├── main.py ← 진입점, asyncio 루프
│ ├── config.py ← 전략 파라미터
│ │
│ ├── ai/ ← [신규] AI 판단 레이어
│ │ ├── context_builder.py ← 데이터 수집 + Claude API 호출
│ │ ├── news_crawler.py ← 네이버 금융 뉴스 크롤링
│ │ └── prompts.py ← 프롬프트 템플릿 관리
│ │
│ ├── data/
│ │ ├── collector.py ← KIS WebSocket 시세 수신
│ │ ├── universe.py ← 종목 풀 갱신 (08:30)
│ │ └── backfill.py ← 과거 분봉 백필
│ │
│ ├── strategy/
│ │ ├── base.py ← 전략 추상 클래스
│ │ └── volatility_breakout.py ← 변동성 돌파 전략 (AI 필터 포함)
│ │
│ ├── risk/
│ │ └── manager.py ← 손절/일일한도/강제청산
│ │
│ ├── execution/
│ │ ├── kis_client.py ← KIS REST/WebSocket 래퍼
│ │ └── order_executor.py ← 주문 전송, 재시도
│ │
│ ├── monitor/
│ │ ├── notifier.py ← 텔레그램 알림
│ │ └── dashboard.py ← Streamlit 대시보드
│ │
│ └── db/
│ ├── models.py ← SQLite 스키마
│ └── repository.py ← DB 접근 레이어
├── kill_switch/
│ └── kill.py
├── backtest/
│ ├── run_backtest.py
│ └── results/
├── data/
│ ├── stockbot.db
│ ├── universe_cache.json
│ └── daily_context.json ← [신규] AI 판단 결과 파일
└── logs/
├── trades.log
└── ai_context.log ← [신규] AI 판단 이력 보관
```
---
## 5. 전략 (확정: 변동성 돌파 + AI 필터)
### 핵심 파라미터 (config.py)
```python
# 변동성 돌파 파라미터
STRATEGY_K = 0.5
ENTRY_START = "09:00"
ENTRY_END = "14:30"
FORCE_EXIT = "14:50"
TP1_PCT = 0.02
TP2_PCT = 0.03
SL_PCT = 0.015
MAX_HOLD_MIN = 120
# AI 판단 파라미터 (신규)
AI_CONTEXT_PATH = "/app/data/daily_context.json"
AI_MIN_SCORE = 40 # sentiment_score 40 미만 시 trade_allowed=false 강제
AI_BOOST_MULTI = 1.5 # boosted_tickers 진입 비중 배율
AI_RISK_SL_MAP = { # risk_level별 손절 강화
"낮음": 0.015,
"보통": 0.015,
"높음": 0.010 # 고위험 장세엔 손절 타이트하게
}
```
### 진입 로직 (AI 필터 통합)
```
목표가 = 당일 시가 + (전일 고가 - 전일 저가) × K
진입 조건 (전부 충족):
[기술적 조건]
1. 현재가 >= 목표가
2. 09:00 ~ 14:30
3. KOSPI 등락률 > -1.0%
4. 전일 거래대금 >= 100억
5. 시가총액 1,000억 ~ 3조
6. VI 미발동
7. 보유 종목 수 < 2
8. 일일 누적 손실 < -3%
[AI 판단 조건]
9. trade_allowed == true
10. 종목 섹터 not in avoid_sectors
11. 종목 not in blacklist_tickers
```
### 청산 로직 (우선순위)
```
1순위: 14:50 강제 청산 (시장가)
2순위: 손절 → 현재가 <= 매수가 × (1 - SL_PCT) [risk_level에 따라 조정]
3순위: 1차 익절 → +TP1_PCT 도달 → 50% 지정가
4순위: 2차 익절 → +TP2_PCT 도달 → 전량 지정가
5순위: 시간 청산 → MAX_HOLD_MIN 경과 + 무수익
```
---
## 6. 종목 유니버스 (Universe)
매일 08:30 자동 갱신.
**1차 필터 (정적)**
| 조건 | 기준 |
|------|------|
| 시장 | 코스피 + 코스닥 |
| 시가총액 | 1,000억 ~ 3조 |
| 상장 기간 | 6개월 이상 |
| 제외 | 관리종목, 거래정지, 우선주, 스팩, ETF, ETN |
**2차 필터 (동적)**
| 조건 | 기준 |
|------|------|
| 전일 거래대금 | 100억 이상 |
| 5일 평균 거래대금 | 50억 이상 |
| 전일 등락률 | -3% ~ +15% |
| 60일 이평선 | 현재가 위 |
**AI 보정**
- boosted_tickers → 우선 감시 목록 상단 배치
- blacklist_tickers → 당일 유니버스에서 즉시 제거
- avoid_sectors → 해당 섹터 전체 진입 금지
**최대 감시 종목: 30개** (WebSocket 안정성 기준)
---
## 7. 리스크 관리
### 포지션 규칙
| 항목 | 기본값 | AI 조정 |
|------|--------|--------|
| 1종목 최대 비중 | 총자산 × 20% | × position_size_multiplier |
| 동시 보유 최대 | 2종목 | risk_level=높음 시 1종목 |
| 손절 기준 | -1.5% | risk_level=높음 시 -1.0% |
### 손실 한도 (계층별)
| 레벨 | 조건 | 동작 |
|------|------|------|
| L1 | 1회 매매 -1.5% | 즉시 손절 |
| L2 | 일일 누적 -3% | 당일 신규 진입 중단 |
| L3 | 3연속 손절 | 당일 매매 중단 |
| L4 | 주간 누적 -7% | 주말까지 중단 + 텔레그램 경고 |
| L5 | 월간 누적 -15% | 전략 폐기 + 백테스트 재실행 |
### 안전장치
- VI 발동 → 해당 종목 즉시 청산
- 호가 스프레드 > 0.5% → 진입 금지
- 매수 후 5분 미체결 → 주문 취소
- WebSocket 끊김 → 보유 포지션 즉시 시장가 청산
- API 오류 10건/분 → kill-switch 자동 실행
- Claude API 오류 → daily_context 없을 시 보수적 기본값으로 fallback
```python
# Claude API 장애 시 fallback
DEFAULT_CONTEXT = {
"trade_allowed": True,
"market_sentiment": "중립",
"sentiment_score": 50,
"risk_level": "보통",
"hot_sectors": [],
"avoid_sectors": [],
"boosted_tickers": [],
"blacklist_tickers": [],
"position_size_multiplier": 0.8, # 보수적으로 축소
"reason": "AI 판단 실패 - 기본값 적용"
}
```
---
## 8. KIS API 활용 전체 목록 (v2.0)
### 기존 (실행 레이어)
| API | 용도 | 방식 |
|-----|------|------|
| H0STCNT0 | 실시간 체결가 | WebSocket |
| H0STASP0 | 실시간 호가 | WebSocket |
| H0STVI0 | VI 발동/해제 | WebSocket |
| TTTC0802U | 주식 매수 주문 | REST POST |
| TTTC0801U | 주식 매도 주문 | REST POST |
| TTTC8001R | 잔고 조회 | REST GET |
### 신규 (AI 판단 레이어)
| API | 용도 | 수집 시각 |
|-----|------|---------|
| FHKST01010100 | 종목 현재가 (KOSPI/KOSDAQ 지수) | 07:40 |
| FHKST03010100 | 업종별 등락률 | 07:40 |
| FHPST01710000 | 거래량 순위 상위 30 | 07:40 |
| FHPST01700000 | 등락률 순위 상위 30 | 07:40 |
| FHKST04430000 | 외국인/기관 순매수 가집계 | 07:40 |
> 위 API 호출은 모두 장 시작 전 단 1회 → rate limit 부담 없음
---
## 9. 스케줄 타임라인 (v2.0)
```
07:30 │ [AI] 네이버 금융 뉴스 크롤링 (전일 헤드라인 수집)
07:40 │ [AI] KIS REST API → 수급/순위/업종 데이터 수집
08:00 │ [AI] Claude API 호출 → daily_context.json 생성
│ 텔레그램 알림: "[AI분석] 오늘 시장: 중립 / 반도체 주목"
08:30 │ Universe 갱신 + AI 블랙리스트 적용
08:50 │ WebSocket 연결, 30종목 구독
│ 목표가 계산 (변동성 돌파)
09:00 │ ─────────── 진입 허용 시작 ───────────
│ 1초 단위 asyncio 루프 시작
11:30 │ 신규 진입 중단 (점심)
13:00 │ 신규 진입 재개
14:30 │ 신규 진입 마감
14:50 │ ─────────── 강제 청산 ───────────────
15:00 │ WebSocket 종료
15:10 │ 당일 결산 로그
15:20 │ 텔레그램 일일 결산 알림
15:30 │ 대기
```
---
## 10. DB 스키마 (SQLite, v2.0)
```sql
-- 기존 체결 내역
CREATE TABLE trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
ticker TEXT NOT NULL,
entry_time TEXT NOT NULL,
exit_time TEXT,
entry_price REAL NOT NULL,
exit_price REAL,
quantity INTEGER NOT NULL,
side TEXT NOT NULL,
exit_reason TEXT,
pnl REAL,
fee REAL,
slippage REAL,
strategy TEXT DEFAULT 'VB',
ai_boosted INTEGER DEFAULT 0 -- [신규] AI boosted 여부
);
-- 기존 일일 요약
CREATE TABLE daily_summary (
date TEXT PRIMARY KEY,
total_trades INTEGER,
win_trades INTEGER,
lose_trades INTEGER,
gross_pnl REAL,
total_fee REAL,
net_pnl REAL,
max_drawdown REAL,
trading_stopped INTEGER DEFAULT 0
);
-- 포지션 (장중 현황)
CREATE TABLE positions (
ticker TEXT PRIMARY KEY,
entry_time TEXT,
entry_price REAL,
quantity INTEGER,
tp1_done INTEGER DEFAULT 0,
target_price REAL,
stop_price REAL
);
-- [신규] AI 판단 이력
CREATE TABLE ai_context_log (
date TEXT PRIMARY KEY,
generated_at TEXT,
trade_allowed INTEGER,
market_sentiment TEXT,
sentiment_score INTEGER,
risk_level TEXT,
hot_sectors TEXT, -- JSON 배열
avoid_sectors TEXT, -- JSON 배열
boosted_tickers TEXT, -- JSON 배열
blacklist_tickers TEXT, -- JSON 배열
position_size_mult REAL,
reason TEXT,
claude_tokens_used INTEGER,
api_call_success INTEGER DEFAULT 1
);
```
---
## 11. 알림 설계 (텔레그램, v2.0)
| 이벤트 | 메시지 형식 |
|--------|-----------|
| AI 분석 완료 | `[AI분석] 시장: 중립(62점) / 주목: 반도체,2차전지 / 회피: 금융` |
| AI 진입 차단 | `[AI차단] 삼성전자 진입 차단 - 금융 섹터 회피` |
| 매수 (일반) | `[매수] 삼성전자 74,000원 / 목표 75,480 / 손절 72,890` |
| 매수 (AI부스트) | `[매수★] 하이닉스 185,000원 / AI 추천 종목` |
| 1차 익절 | `[익절1] 삼성전자 +2.1% / 잔여 50%` |
| 손절 | `[손절] 삼성전자 -1.5% / 즉시 청산` |
| 강제 청산 | `[14:50 강제청산] 전 포지션 청산 완료` |
| L2 발동 | `[경고] 일일 손실 -3% 도달. 오늘 매매 중단.` |
| 일일 결산 | `[결산] 매매 5회 / 승 3 패 2 / 순손익 +1.2% / AI 정확도: 3/5` |
| 장애 | `[긴급] WebSocket 끊김. kill-switch 실행.` |
| AI 실패 | `[경고] AI 판단 실패. 기본값 적용 (비중 80%).` |
---
## 12. 비용 분석 (월간)
### Claude API 토큰 사용량 추정
| 작업 | 빈도 | 토큰/회 | 월간 토큰 |
|------|------|---------|---------|
| 일일 시장 판단 | 1회/일 × 22거래일 | ~3,000 | ~66,000 |
| fallback 재시도 | 0~2회/월 | ~3,000 | ~6,000 |
| **합계** | | | **~72,000 토큰/월** |
**월 비용 (Claude Sonnet 4): 약 $2~3 (한화 약 3,000~4,000원)**
### 전체 운영 비용
| 항목 | 월 비용 |
|------|--------|
| KIS API | 무료 |
| Claude API | ~$3 |
| 네이버 크롤링 | 무료 |
| Synology NAS 전기세 | 기존 운영 중이면 추가 없음 |
| **합계** | **~$3/월** |
---
## 13. 백테스트 계획 (v2.0)
### AI 판단 레이어 검증 방법
AI 판단은 백테스트에 직접 포함 불가 (과거 뉴스 재현 어려움).
따라서 2단계로 분리 검증:
```
1단계: 규칙 기반 백테스트 (기존)
- 변동성 돌파 전략 단독 성과 측정
- K값 최적화, 수수료/슬리피지 반영
2단계: AI 필터 사후 분석 (모의투자 후)
- 3개월 모의투자 후 daily_context.json 이력 vs 실제 결과 대조
- AI가 "avoid" 판단한 날 손실률 vs "trade_allowed" 날 수익률 비교
- AI 판단 정확도 계산 → 임계값 재조정
```
### 합격 기준 (out-of-sample)
| 지표 | 통과 기준 | 목표 |
|------|----------|------|
| 샤프지수 | > 1.0 | > 1.5 |
| MDD | < 15% | < 10% |
| 승률 | > 45% | > 55% |
| 손익비 | > 1.3 | > 1.8 |
| AI 차단 정확도 | > 55% | > 65% |
---
## 14. 인프라 (docker-compose.yml)
```yaml
version: "3.9"
services:
redis:
image: redis:7-alpine
container_name: stockbot-redis
restart: unless-stopped
volumes:
- ./data/redis:/data
stockbot:
build: ./app
container_name: stockbot-main
restart: unless-stopped
depends_on:
- redis
env_file: .env
volumes:
- ./data:/app/data
- ./logs:/app/logs
environment:
- TZ=Asia/Seoul
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
kill-switch:
build: ./kill_switch
container_name: stockbot-killswitch
restart: "no"
env_file: .env
profiles: ["emergency"]
dashboard:
build: ./monitor
container_name: stockbot-dashboard
restart: unless-stopped
ports:
- "8501:8501"
volumes:
- ./data:/app/data
environment:
- TZ=Asia/Seoul
```
### .env 구조 (v2.0)
```
# KIS API
KIS_APP_KEY=...
KIS_APP_SECRET=...
KIS_ACCOUNT_NO=...
KIS_MOCK=true
# Claude API (신규)
ANTHROPIC_API_KEY=...
CLAUDE_MODEL=claude-sonnet-4-20250514
# 텔레그램
TELEGRAM_TOKEN=...
TELEGRAM_CHAT_ID=...
# Redis
REDIS_HOST=stockbot-redis
REDIS_PORT=6379
# 운영 모드
DRY_RUN=true
LOG_LEVEL=INFO
AI_ENABLED=true # false 시 AI 레이어 비활성화, 기본값 사용
```
---
## 15. 개발 로드맵 (v2.0)
### Phase 1 — 데이터 기반 구축 (2주)
- [ ] KIS Open API 신청 + 모의투자 계좌
- [ ] pykrx 분봉 데이터 백필 (2021~2024)
- [ ] SQLite 스키마 (v2.0 포함)
- [ ] Universe 스캐너 구현
- [ ] Docker Compose NAS 기동 확인
### Phase 2 — 백테스트 (3주)
- [ ] vectorbt 변동성 돌파 백테스트
- [ ] 수수료/슬리피지 반영
- [ ] K값 그리드 서치 + out-of-sample 검증
### Phase 3 — KIS 연동 + dry-run (2주)
- [ ] KIS REST/WebSocket 클라이언트
- [ ] OrderExecutor (시장가/지정가/IOC)
- [ ] DRY_RUN=true 신호 발생 확인
### Phase 4 — AI 레이어 구현 (2주) ← 신규
- [ ] 네이버 금융 뉴스 크롤러 구현
- [ ] KIS 순위/수급 API 수집 모듈
- [ ] Claude API 연동 + 프롬프트 튜닝
- [ ] daily_context.json 생성 파이프라인
- [ ] fallback 로직 구현
- [ ] ai_context_log DB 저장
### Phase 5 — 리스크 + 알림 + 대시보드 (2주)
- [ ] RiskManager L1~L5 구현
- [ ] kill-switch 컨테이너
- [ ] 텔레그램 Notifier (AI 메시지 포함)
- [ ] Streamlit 대시보드 (AI 판단 현황 패널 추가)
### Phase 6 — 모의투자 실운영 (최소 3개월)
- [ ] KIS_MOCK=true, DRY_RUN=false
- [ ] 매일 결산 로그 + AI 정확도 추적
- [ ] 3개월 후 AI 필터 임계값 재조정
- [ ] 샤프 > 1.0, MDD < 15% 달성 시 Phase 7
### Phase 7 — 소액 실거래 (무기한)
- [ ] KIS_MOCK=false
- [ ] 총자산 5% 한도로 시작
- [ ] 1개월 단위 성과 검토
---
## 16. 보안 체크리스트
- [ ] .env → .gitignore 등록
- [ ] KIS/Anthropic API 키 → GitHub 절대 금지
- [ ] ANTHROPIC_API_KEY 월 1회 암호화 백업
- [ ] Synology 방화벽: 8501 내부망만 허용
- [ ] 텔레그램 봇: chat_id 화이트리스트
- [ ] 주문 로그 SQLite + logs/ 이중 보관
- [ ] AI 판단 이력 ai_context_log 영구 보관
---
## 17. 절대 금지 (하드코딩)
```python
BLACKLIST_REASONS = [
"신규상장 6개월 미만", "관리종목", "투자경고",
"거래정지", "우선주", "스팩", "ETF/ETN",
]
TRADING_BLACKOUT = [
("11:30", "13:00"), # 점심
("14:50", "15:30"), # 마감
("08:00", "09:00"), # 동시호가
]
HARD_EXIT_TIME = "14:50" # 절대 변경 불가
# AI가 trade_allowed=false 반환해도 이미 보유 중인 포지션 청산은 진행
# AI는 신규 진입만 차단, 청산 로직에는 관여하지 않음
AI_SCOPE = "ENTRY_ONLY" # 절대 변경 불가
```
---
## 18. 면책 조항
> 본 기획서는 시스템 설계 문서이며, 투자 수익을 보장하지 않는다.
> AI 판단 레이어는 보조 필터일 뿐, 수익을 보장하지 않는다.
> 단타는 개인투자자의 90% 이상이 손실을 보는 영역이다.
> 반드시 모의투자 3개월 이상 검증 후 실거래 전환할 것.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1021 KiB