Stop tracking runtime and generated files
@@ -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/
|
||||
|
||||
@@ -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>
|
||||
@@ -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>© 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) overlay.classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 927 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 1019 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 25 KiB |
@@ -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개월 이상 검증 후 실거래 전환할 것.
|
||||
|
Before Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1021 KiB |