프론트엔드 추가 및 자동매매 로직 개선:
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 1m42s
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 1m42s
- Svelte 기반 프론트엔드 프로젝트 초기 설정 추가 (`vite`, `tailwindcss` 등 포함). - "자동매매" 주요 상태 및 규칙 관리 페이지 구현. - 1차/2차 손절 및 익절 조건 평가 로직 추가(`calcStopTargets`, `evalExitReason` 등). - 포지션 상세 로그 및 WebSocket 기반 실시간 로그 스트림 추가. - API 서비스 및 Frontend 간 Proxy 설정(Vite 서버). - 세션 체크를 위한 `CheckSession` 핸들러 추가.
This commit is contained in:
570
frontend/src/routes/(app)/autotrade/+page.svelte
Normal file
570
frontend/src/routes/(app)/autotrade/+page.svelte
Normal file
@@ -0,0 +1,570 @@
|
||||
<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' | 'logs'>('rules')
|
||||
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, l, ws, themes] = await Promise.all([
|
||||
autotradeApi.getStatus(),
|
||||
autotradeApi.getRules(),
|
||||
autotradeApi.getPositions(),
|
||||
autotradeApi.getLogs(),
|
||||
autotradeApi.getWatchSource(),
|
||||
stockApi.getThemes(),
|
||||
])
|
||||
status = s
|
||||
rules = r
|
||||
positions = p
|
||||
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.positions}개</span>
|
||||
<span class="text-sm text-gray-400">오늘 {status.todayTrades}건</span>
|
||||
{#if status.todayProfit !== 0}
|
||||
<span class="text-sm {priceClass(status.todayProfit)}">
|
||||
오늘 손익 {status.todayProfit >= 0 ? '+' : ''}{formatPrice(status.todayProfit)}
|
||||
</span>
|
||||
{/if}
|
||||
{/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})`], ['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 === '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>
|
||||
Reference in New Issue
Block a user