Files
stocksearch/frontend/src/routes/(app)/autotrade/+page.svelte
hayato5246 5aeb5f2b80
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 11m20s
자산 현황 및 자동매매 페이지 제거:
- `/templates/pages/asset.html`, `/templates/pages/autotrade.html` HTML 템플릿 삭제.
- `/static/js/asset.js`, `/static/js/autotrade.js` 클라이언트 스크립트 제거.
- 관련 함수 및 초기화 로직 삭제 (자산 조회 및 자동매매 기능 비활성화).
2026-04-07 21:43:24 +09:00

627 lines
25 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import { autotradeApi } from '$lib/api/autotrade'
import { stockApi } from '$lib/api/stock'
import { tradeLogStream, wsStore } from '$lib/stores/ws'
import Modal from '$lib/components/Modal.svelte'
import { formatPrice, formatRate, priceClass } from '$lib/utils'
import type {
AutoTradeRule, AutoTradePosition, AutoTradeLog, AutoTradeStatus,
AutoTradeWatchSource, ThemeGroup, ThemeRef
} from '$lib/api/types'
let status = $state<AutoTradeStatus | null>(null)
let rules = $state<AutoTradeRule[]>([])
let positions = $state<AutoTradePosition[]>([])
let logs = $state<AutoTradeLog[]>([])
let activeTab = $state<'rules' | 'positions' | 'trades' | 'logs'>('rules')
let trades = $state<AutoTradePosition[]>([])
let showRuleModal = $state(false)
let editingRule = $state<Partial<AutoTradeRule> | null>(null)
let loading = $state(true)
let actionLoading = $state(false)
let emergencyConfirm = $state(false)
// 감시 소스 상태
let watchSource = $state<AutoTradeWatchSource>({ useScanner: true, selectedThemes: [] })
let allThemes = $state<ThemeGroup[]>([])
let themeSearch = $state('')
let showThemeDropdown = $state(false)
let filteredThemes = $derived(
allThemes
.filter(t =>
!watchSource.selectedThemes.some(s => s.code === t.code) &&
(!themeSearch || t.name.toLowerCase().includes(themeSearch.toLowerCase()))
)
.sort((a, b) => a.name.localeCompare(b.name, 'ko'))
)
// WebSocket으로 자동매매 로그 수신
const logUnsubscribe = tradeLogStream.subscribe((log) => {
if (log) {
logs = [log, ...logs].slice(0, 200)
}
})
onMount(async () => {
wsStore.connect()
await loadAll()
loading = false
})
onDestroy(() => {
logUnsubscribe()
})
async function loadAll() {
try {
const [s, r, p, t, l, ws, themes] = await Promise.all([
autotradeApi.getStatus(),
autotradeApi.getRules(),
autotradeApi.getPositions(),
autotradeApi.getTrades(),
autotradeApi.getLogs(),
autotradeApi.getWatchSource(),
stockApi.getThemes(),
])
status = s
rules = r
positions = p
trades = t ?? []
logs = l
watchSource = ws
allThemes = themes
} catch (e) {
console.error(e)
}
}
async function toggleEngine() {
actionLoading = true
try {
if (status?.running) {
await autotradeApi.stop()
status = { ...status!, running: false }
} else {
await autotradeApi.start()
status = { ...status!, running: true }
}
} finally {
actionLoading = false
}
}
async function handleEmergency() {
if (!emergencyConfirm) {
emergencyConfirm = true
setTimeout(() => { emergencyConfirm = false }, 5000)
return
}
emergencyConfirm = false
actionLoading = true
try {
await autotradeApi.emergency()
positions = []
} finally {
actionLoading = false
}
}
// 감시 소스 토글
async function toggleScanner() {
const updated = { ...watchSource, useScanner: !watchSource.useScanner }
try {
await autotradeApi.setWatchSource(updated)
watchSource = updated
} catch (e) {
console.error(e)
}
}
// 테마 추가
async function addTheme(theme: ThemeGroup) {
const ref: ThemeRef = { code: theme.code, name: theme.name }
const updated = {
...watchSource,
selectedThemes: [...watchSource.selectedThemes, ref],
}
try {
await autotradeApi.setWatchSource(updated)
watchSource = updated
themeSearch = ''
showThemeDropdown = false
} catch (e) {
console.error(e)
}
}
// 테마 제거
async function removeTheme(code: string) {
const updated = {
...watchSource,
selectedThemes: watchSource.selectedThemes.filter(t => t.code !== code),
}
try {
await autotradeApi.setWatchSource(updated)
watchSource = updated
} catch (e) {
console.error(e)
}
}
// 개별 포지션 청산
async function closePosition(code: string, name: string) {
if (!confirm(`${name}(${code}) 포지션을 청산하시겠습니까?`)) return
try {
await autotradeApi.closePosition(code)
// 포지션 목록 새로고침
positions = await autotradeApi.getPositions()
} catch (e) {
alert(e instanceof Error ? e.message : '청산 실패')
}
}
function openAddRule() {
editingRule = {
name: '새 규칙',
enabled: true,
minRiseScore: 60,
minCntrStr: 110,
requireBullish: false,
orderAmount: 1000000,
maxPositions: 3,
stopLoss1Pct: -2.0,
stopLoss1Count: 3,
stopLossPct: -4.0,
takeProfitPct: 5.0,
maxHoldMinutes: 60,
exitBeforeClose: true,
}
showRuleModal = true
}
function openEditRule(rule: AutoTradeRule) {
editingRule = { ...rule }
showRuleModal = true
}
async function saveRule() {
if (!editingRule) return
actionLoading = true
try {
if (editingRule.id) {
const updated = await autotradeApi.updateRule(editingRule.id, editingRule)
rules = rules.map(r => r.id === updated.id ? updated : r)
} else {
const newRule = await autotradeApi.addRule(editingRule as Omit<AutoTradeRule, 'id'|'createdAt'>)
rules = [...rules, newRule]
}
showRuleModal = false
} catch (e: unknown) {
alert(e instanceof Error ? e.message : '저장 실패')
} finally {
actionLoading = false
}
}
async function deleteRule(id: string) {
if (!confirm('규칙을 삭제하시겠습니까?')) return
await autotradeApi.deleteRule(id)
rules = rules.filter(r => r.id !== id)
}
async function toggleRule(id: string) {
const updated = await autotradeApi.toggleRule(id)
rules = rules.map(r => r.id === id ? updated : r)
}
function logLevelClass(level: string): string {
if (level === 'error') return 'text-red-400'
if (level === 'warn') return 'text-yellow-400'
return 'text-gray-300'
}
function formatTime(iso: string): string {
if (!iso) return ''
const d = new Date(iso)
return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`
}
</script>
<svelte:head>
<title>자동매매 - 주식 시세</title>
</svelte:head>
<!-- 상단 상태 바 -->
<div class="flex items-center justify-between mb-6 flex-wrap gap-4">
<div class="flex items-center gap-4">
<h1 class="text-xl font-bold text-white">자동매매</h1>
{#if status}
<span class="flex items-center gap-1.5 text-sm {status.running ? 'text-green-400' : 'text-gray-400'}">
<span class="w-2 h-2 rounded-full {status.running ? 'bg-green-400 animate-pulse' : 'bg-gray-600'}"></span>
{status.running ? '실행 중' : '중지됨'}
</span>
<span class="text-sm text-gray-400">포지션 {status.activePositions}</span>
<span class="text-sm text-gray-400">오늘 {status.tradeCount}</span>
<span class="text-sm {status.totalPL !== 0 ? priceClass(status.totalPL) : 'text-gray-400'}">
오늘 손익 {status.totalPL >= 0 ? '+' : ''}{formatPrice(status.totalPL)}
</span>
{/if}
</div>
<div class="flex gap-2">
<button
onclick={toggleEngine}
disabled={actionLoading}
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50
{status?.running
? 'bg-gray-700 text-white hover:bg-gray-600'
: 'bg-green-600 text-white hover:bg-green-500'}"
>
{status?.running ? '⏹ 중지' : '▶ 시작'}
</button>
<button
onclick={handleEmergency}
disabled={actionLoading}
class="px-4 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50
{emergencyConfirm
? 'bg-red-500 text-white animate-pulse'
: 'bg-red-900/50 text-red-300 hover:bg-red-800/50'}"
>
{emergencyConfirm ? '⚠ 한번 더 클릭하면 전량 청산' : '🚨 긴급청산'}
</button>
</div>
</div>
<!-- 감시 소스 설정 -->
<div class="bg-gray-800 rounded-xl p-4 mb-6">
<h2 class="text-sm font-semibold text-white mb-3">감시 소스</h2>
<div class="flex flex-wrap items-start gap-6">
<!-- 체결강도 자동감지 토글 -->
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer shrink-0">
<button
onclick={toggleScanner}
class="w-10 h-5 rounded-full transition-colors relative {watchSource.useScanner ? 'bg-green-600' : 'bg-gray-600'}"
>
<span class="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full transition-transform {watchSource.useScanner ? 'translate-x-5' : ''}"></span>
</button>
체결강도 자동감지
<span class="text-xs text-gray-500">(거래량 상위 20종목)</span>
</label>
<!-- 테마 선택 -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
<span class="text-sm text-gray-400 shrink-0">감시 테마</span>
<div class="relative flex-1">
<input
type="text"
bind:value={themeSearch}
onfocus={() => { showThemeDropdown = true }}
onblur={() => { setTimeout(() => { showThemeDropdown = false }, 200) }}
placeholder="테마 검색..."
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-1.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500"
/>
{#if showThemeDropdown && filteredThemes.length > 0}
<div class="absolute top-full left-0 right-0 mt-1 bg-gray-700 border border-gray-600 rounded-lg shadow-xl z-10 max-h-48 overflow-auto">
{#each filteredThemes as theme (theme.code)}
<button
onmousedown={() => addTheme(theme)}
class="w-full px-3 py-2 text-left text-sm text-gray-200 hover:bg-gray-600 transition-colors flex justify-between items-center"
>
<span>{theme.name}</span>
<span class="text-xs {priceClass(theme.fluRt)}">{formatRate(theme.fluRt)}</span>
</button>
{/each}
</div>
{/if}
</div>
</div>
{#if watchSource.selectedThemes.length > 0}
<div class="flex flex-wrap gap-2">
{#each watchSource.selectedThemes as theme (theme.code)}
<span class="inline-flex items-center gap-1 px-2.5 py-1 bg-blue-900/40 text-blue-300 text-xs rounded-full">
{theme.name}
<button
onclick={() => removeTheme(theme.code)}
class="text-blue-400 hover:text-blue-200 ml-0.5"
>×</button>
</span>
{/each}
</div>
{:else}
<div class="text-xs text-gray-500">선택된 테마 없음</div>
{/if}
</div>
</div>
</div>
<!-- 탭 -->
<div class="flex gap-1 mb-4 border-b border-gray-700 pb-1">
{#each [['rules', `규칙 (${rules.length})`], ['positions', `포지션 (${positions.length})`], ['trades', `거래 (${trades.length})`], ['logs', '로그']] as [tab, label]}
<button
onclick={() => { activeTab = tab as typeof activeTab }}
class="px-4 py-2 text-sm transition-colors {activeTab === tab ? 'text-blue-400 border-b-2 border-blue-400' : 'text-gray-400 hover:text-white'}"
>{label}</button>
{/each}
</div>
<!-- 규칙 탭 -->
{#if activeTab === 'rules'}
<div class="mb-3">
<button
onclick={openAddRule}
class="bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors"
>+ 규칙 추가</button>
</div>
{#if rules.length === 0}
<div class="bg-gray-800 rounded-xl p-8 text-center text-gray-500">규칙을 추가해보세요</div>
{:else}
<div class="space-y-3">
{#each rules as rule (rule.id)}
<div class="bg-gray-800 rounded-xl p-4">
<div class="flex items-start justify-between">
<div>
<div class="flex items-center gap-3">
<span class="font-semibold text-white">{rule.name}</span>
<span class="text-xs px-2 py-0.5 rounded {rule.enabled ? 'bg-green-900/50 text-green-400' : 'bg-gray-700 text-gray-500'}">
{rule.enabled ? '활성' : '비활성'}
</span>
</div>
<div class="text-xs text-gray-400 mt-2 flex flex-wrap gap-3">
<span>진입점수 {rule.minRiseScore}</span>
<span>체결강도 {rule.minCntrStr}</span>
<span>주문금액 {formatPrice(rule.orderAmount)}</span>
<span>최대 {rule.maxPositions}종목</span>
<span>익절 +{rule.takeProfitPct}%</span>
<span>손절 {rule.stopLossPct}%</span>
{#if rule.maxHoldMinutes > 0}
<span>최대 {rule.maxHoldMinutes}</span>
{/if}
</div>
</div>
<div class="flex gap-2 shrink-0">
<button
onclick={() => toggleRule(rule.id)}
class="text-xs px-3 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300 transition-colors"
>{rule.enabled ? '비활성화' : '활성화'}</button>
<button
onclick={() => openEditRule(rule)}
class="text-xs px-3 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300 transition-colors"
>수정</button>
<button
onclick={() => deleteRule(rule.id)}
class="text-xs px-3 py-1 rounded bg-red-900/50 hover:bg-red-800/50 text-red-300 transition-colors"
>삭제</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
<!-- 포지션 탭 -->
{:else if activeTab === 'positions'}
{#if positions.length === 0}
<div class="bg-gray-800 rounded-xl p-8 text-center text-gray-500">보유 포지션 없음</div>
{:else}
<div class="bg-gray-800 rounded-xl overflow-hidden">
<table>
<thead>
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400">종목</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">수량</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">매수가</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">손절가</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">익절가</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">상태</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">진입시각</th>
<th class="px-3 py-3 text-center text-xs font-medium text-gray-400"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
{#each positions as pos (pos.code + pos.orderNo)}
<tr class="hover:bg-gray-700">
<td class="px-4 py-3">
<div class="text-sm font-medium text-white">{pos.name}</div>
<div class="text-xs text-gray-500">{pos.code}</div>
</td>
<td class="px-3 py-3 text-right text-sm text-gray-300">{pos.qty}</td>
<td class="px-3 py-3 text-right text-sm font-mono text-gray-300">{formatPrice(pos.buyPrice)}</td>
<td class="px-3 py-3 text-right text-sm text-blue-400">{formatPrice(pos.stopLoss)}</td>
<td class="px-3 py-3 text-right text-sm text-red-400">{formatPrice(pos.takeProfit)}</td>
<td class="px-3 py-3 text-right">
<span class="text-xs px-2 py-0.5 rounded {
pos.status === 'open' ? 'bg-green-900/50 text-green-400' :
pos.status === 'pending' ? 'bg-yellow-900/50 text-yellow-400' :
'bg-gray-700 text-gray-400'
}">{pos.status}</span>
</td>
<td class="px-3 py-3 text-right text-xs text-gray-400">{formatTime(pos.entryTime)}</td>
<td class="px-3 py-3 text-center">
{#if pos.status === 'open' || pos.status === 'pending'}
<button
onclick={() => closePosition(pos.code, pos.name)}
class="text-xs px-2.5 py-1 rounded bg-orange-900/50 hover:bg-orange-800/50 text-orange-300 transition-colors"
>청산</button>
{:else if pos.exitReason}
<span class="text-xs text-gray-500">{pos.exitReason}</span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<!-- 거래 탭 -->
{:else if activeTab === 'trades'}
{#if trades.length === 0}
<div class="bg-gray-800 rounded-xl p-8 text-center text-gray-500">거래 내역 없음</div>
{:else}
<div class="bg-gray-800 rounded-xl overflow-hidden">
<table>
<thead>
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400">종목</th>
<th class="px-3 py-3 text-center text-xs font-medium text-gray-400">구분</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">수량</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">매수가</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">매도가</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">손익</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">수익률</th>
<th class="px-3 py-3 text-center text-xs font-medium text-gray-400">사유</th>
<th class="px-3 py-3 text-right text-xs font-medium text-gray-400">청산시각</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
{#each trades as trade (trade.orderNo + (trade.exitTime ?? ''))}
{@const pl = ((trade.exitPrice ?? 0) - trade.buyPrice) * trade.qty}
{@const plRate = trade.buyPrice > 0 ? ((trade.exitPrice ?? 0) - trade.buyPrice) / trade.buyPrice * 100 : 0}
<tr class="hover:bg-gray-700">
<td class="px-4 py-3">
<div class="text-sm font-medium text-white">{trade.name}</div>
<div class="text-xs text-gray-500">{trade.code}</div>
</td>
<td class="px-3 py-3 text-center">
<span class="text-xs px-2 py-0.5 rounded bg-blue-900/50 text-blue-300">매도</span>
</td>
<td class="px-3 py-3 text-right text-sm text-gray-300">{trade.qty}</td>
<td class="px-3 py-3 text-right text-sm font-mono text-gray-300">{formatPrice(trade.buyPrice)}</td>
<td class="px-3 py-3 text-right text-sm font-mono {priceClass(pl)}">{formatPrice(trade.exitPrice ?? 0)}</td>
<td class="px-3 py-3 text-right text-sm font-mono {priceClass(pl)}">
{pl >= 0 ? '+' : ''}{formatPrice(pl)}
</td>
<td class="px-3 py-3 text-right text-sm {priceClass(plRate)}">
{plRate >= 0 ? '+' : ''}{plRate.toFixed(2)}%
</td>
<td class="px-3 py-3 text-center">
<span class="text-xs px-2 py-0.5 rounded {
trade.exitReason === '익절' ? 'bg-red-900/50 text-red-300' :
trade.exitReason === '손절' ? 'bg-blue-900/50 text-blue-300' :
'bg-gray-700 text-gray-400'
}">{trade.exitReason ?? '-'}</span>
</td>
<td class="px-3 py-3 text-right text-xs text-gray-400">{formatTime(trade.exitTime ?? '')}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<!-- 로그 탭 -->
{:else if activeTab === 'logs'}
<div class="bg-gray-800 rounded-xl p-4 font-mono text-xs space-y-1 max-h-[60vh] overflow-auto">
{#if logs.length === 0}
<div class="text-gray-500 text-center py-8">로그 없음</div>
{/if}
{#each logs as log}
<div class="flex gap-3">
<span class="text-gray-500 shrink-0">{formatTime(log.at)}</span>
<span class="shrink-0 {logLevelClass(log.level)}">[{log.level}]</span>
{#if log.code}
<span class="text-yellow-500 shrink-0">{log.code}</span>
{/if}
<span class="{logLevelClass(log.level)}">{log.message}</span>
</div>
{/each}
</div>
{/if}
<!-- 규칙 편집 모달 -->
<Modal title={editingRule?.id ? '규칙 수정' : '규칙 추가'} open={showRuleModal} on:close={() => { showRuleModal = false }}>
{#if editingRule}
<div class="space-y-4">
<div>
<label class="text-xs text-gray-400 mb-1 block">규칙명</label>
<input
type="text"
bind:value={editingRule.name}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white"
/>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="r-minRiseScore" class="text-xs text-gray-400 mb-1 block">최소 진입점수 (0~100)</label>
<input id="r-minRiseScore" type="number" bind:value={editingRule.minRiseScore}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
<div>
<label for="r-minCntrStr" class="text-xs text-gray-400 mb-1 block">최소 체결강도</label>
<input id="r-minCntrStr" type="number" bind:value={editingRule.minCntrStr}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
<div>
<label for="r-orderAmount" class="text-xs text-gray-400 mb-1 block">주문금액 (원)</label>
<input id="r-orderAmount" type="number" bind:value={editingRule.orderAmount}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
<div>
<label for="r-maxPositions" class="text-xs text-gray-400 mb-1 block">최대 보유종목 수</label>
<input id="r-maxPositions" type="number" bind:value={editingRule.maxPositions}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
<div>
<label for="r-stopLoss1Pct" class="text-xs text-gray-400 mb-1 block">1차 손절 % (예: -2)</label>
<input id="r-stopLoss1Pct" type="number" step="0.1" bind:value={editingRule.stopLoss1Pct}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
<div>
<label for="r-stopLoss1Count" class="text-xs text-gray-400 mb-1 block">1차 손절 터치 횟수</label>
<input id="r-stopLoss1Count" type="number" bind:value={editingRule.stopLoss1Count}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
<div>
<label for="r-stopLossPct" class="text-xs text-gray-400 mb-1 block">2차 손절 % (예: -4)</label>
<input id="r-stopLossPct" type="number" step="0.1" bind:value={editingRule.stopLossPct}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
<div>
<label for="r-takeProfitPct" class="text-xs text-gray-400 mb-1 block">익절 % (예: 5)</label>
<input id="r-takeProfitPct" type="number" step="0.1" bind:value={editingRule.takeProfitPct}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
<div>
<label for="r-maxHoldMinutes" class="text-xs text-gray-400 mb-1 block">최대 보유시간 (분, 0=무제한)</label>
<input id="r-maxHoldMinutes" type="number" bind:value={editingRule.maxHoldMinutes}
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-sm text-white" />
</div>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
<input type="checkbox" bind:checked={editingRule.enabled} class="accent-blue-500" />
활성화
</label>
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
<input type="checkbox" bind:checked={editingRule.requireBullish} class="accent-blue-500" />
AI 호재 신호 필요
</label>
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
<input type="checkbox" bind:checked={editingRule.exitBeforeClose} class="accent-blue-500" />
장 마감 전 청산
</label>
</div>
<div class="flex gap-2 pt-2">
<button
onclick={saveRule}
disabled={actionLoading}
class="flex-1 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-sm font-medium py-2 rounded-lg transition-colors"
>
{actionLoading ? '저장 중...' : '저장'}
</button>
<button
onclick={() => { showRuleModal = false }}
class="px-4 bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm rounded-lg transition-colors"
>취소</button>
</div>
</div>
{/if}
</Modal>