매매전략 수정중
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import TradingRules from './pages/TradingRules'
|
||||
import TradeHistory from './pages/TradeHistory'
|
||||
import MCPStatus from './pages/MCPStatus'
|
||||
import StrategySettings from './pages/StrategySettings'
|
||||
import Layout from './components/Layout'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/rules" element={<TradingRules />} />
|
||||
<Route path="/trades" element={<TradeHistory />} />
|
||||
<Route path="/mcp" element={<MCPStatus />} />
|
||||
</Routes>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/trades" element={<TradeHistory />} />
|
||||
<Route path="/mcp" element={<MCPStatus />} />
|
||||
<Route path="/strategy" element={<StrategySettings />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import HtsHeader from './HtsHeader'
|
||||
import './HtsPage.css'
|
||||
|
||||
interface HtsPageProps {
|
||||
@@ -12,7 +11,6 @@ interface HtsPageProps {
|
||||
const HtsPage = ({ children, className, contentClassName }: HtsPageProps) => {
|
||||
return (
|
||||
<div className={clsx('hts-page', className)}>
|
||||
<HtsHeader />
|
||||
<div className={clsx('hts-content', contentClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { LayoutDashboard, FileText, History, Cpu, Activity } from 'lucide-react'
|
||||
import { LayoutDashboard, History, Cpu, Activity, SlidersHorizontal } from 'lucide-react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import api from '../lib/api'
|
||||
import { useWebSocket } from '../hooks/useWebSocket'
|
||||
@@ -20,7 +20,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', icon: LayoutDashboard, label: '대시보드' },
|
||||
{ path: '/rules', icon: FileText, label: '전략 룰' },
|
||||
{ path: '/strategy', icon: SlidersHorizontal, label: '전략 설정' },
|
||||
{ path: '/trades', icon: History, label: '매매 로그' },
|
||||
{ path: '/mcp', icon: Cpu, label: 'MCP 상태' },
|
||||
]
|
||||
|
||||
@@ -8,7 +8,6 @@ import ChartPanel, { AiAnalysisPanel } from '../components/ChartPanel'
|
||||
import OrderbookPanel from '../components/OrderbookPanel'
|
||||
import RecommendationPanel from '../components/RecommendationPanel'
|
||||
import JournalPanel from '../components/JournalPanel'
|
||||
import { Activity } from 'lucide-react'
|
||||
import './Dashboard.css'
|
||||
|
||||
// 마켓 운영 시간 체크 (한국장 + 미국장)
|
||||
@@ -112,7 +111,7 @@ const Dashboard = () => {
|
||||
const portfolioSummary = portfolioCombined?.summary
|
||||
|
||||
// MCP 상태 - 30초 (UI 정보용, 느려도 무방)
|
||||
const { data: mcpStatus } = useQuery({
|
||||
useQuery({
|
||||
queryKey: ['mcp-status'],
|
||||
queryFn: async () => (await api.get('/mcp/status')).data,
|
||||
refetchInterval: isMarketOpen ? 30000 : false,
|
||||
@@ -242,11 +241,11 @@ const Dashboard = () => {
|
||||
if (!portfolio) return
|
||||
setLivePrices(prev => {
|
||||
const next = { ...prev }
|
||||
;(portfolio as any[]).forEach((s: any) => {
|
||||
if (s.current_price && !next[s.stock_code]) {
|
||||
next[s.stock_code] = Number(s.current_price)
|
||||
}
|
||||
})
|
||||
; (portfolio as any[]).forEach((s: any) => {
|
||||
if (s.current_price && !next[s.stock_code]) {
|
||||
next[s.stock_code] = Number(s.current_price)
|
||||
}
|
||||
})
|
||||
return next
|
||||
})
|
||||
}, [portfolio])
|
||||
@@ -502,31 +501,7 @@ const Dashboard = () => {
|
||||
}, [portfolio, ruleBasedRecommendations, selectedStock, handleSelectStock])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f1115] text-gray-100 font-sans flex flex-col h-screen overflow-hidden">
|
||||
|
||||
<header className="border-b border-gray-800 bg-[#161b22] px-6 py-4 flex justify-between items-center z-50 backdrop-blur-md bg-opacity-90 flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-blue-600 p-2 rounded-lg shadow-lg shadow-blue-500/20">
|
||||
<Activity className="text-white" size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
|
||||
AutoStock Trader
|
||||
</h1>
|
||||
<p className="text-xs text-gray-500">Gen 3 AI Algorithm</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-800 rounded-full border border-gray-700">
|
||||
<div className={`w-2 h-2 rounded-full ${wsConnected ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]' : 'bg-red-500'}`} />
|
||||
<span className="text-xs text-gray-400">{wsConnected ? '실시간 연결됨' : '연결 끊김'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-800 rounded-full border border-gray-700">
|
||||
<div className={`w-2 h-2 rounded-full ${mcpStatus?.status === 'connected' ? 'bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]' : 'bg-yellow-500'}`} />
|
||||
<span className="text-xs text-gray-400">MCP: {mcpStatus?.status || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="bg-[#0f1115] text-gray-100 font-sans flex flex-col h-[calc(100vh-4rem)] overflow-hidden">
|
||||
|
||||
<main className="flex-1 p-4 max-w-[1920px] mx-auto w-full flex flex-col gap-4 overflow-hidden">
|
||||
<div className="flex-shrink-0">
|
||||
|
||||
398
frontend/src/pages/StrategySettings.tsx
Normal file
398
frontend/src/pages/StrategySettings.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import api from '../lib/api'
|
||||
import { useState, useMemo } from 'react'
|
||||
import HtsPage from '../components/HtsPage'
|
||||
import { RotateCcw, Save, Power, PowerOff, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react'
|
||||
|
||||
// ── 타입 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface StrategyConfig {
|
||||
id: number
|
||||
key: string
|
||||
group: string
|
||||
label: string
|
||||
description: string | null
|
||||
value_type: string // 'float' | 'int' | 'bool' | 'str'
|
||||
value: string
|
||||
default_value: string
|
||||
min_value: string | null
|
||||
max_value: string | null
|
||||
is_active: boolean
|
||||
updated_by: string | null
|
||||
}
|
||||
|
||||
// ── 그룹 한글 레이블 ──────────────────────────────────────────────────────────
|
||||
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
INDICATOR: '기술 지표',
|
||||
RISK: '리스크 관리',
|
||||
FILTER: '진입 필터',
|
||||
DAY_TRADE: '데이트레이딩',
|
||||
MEAN_REVERSION: '평균회귀 전략',
|
||||
GENERAL: '일반 설정',
|
||||
}
|
||||
|
||||
const GROUP_ORDER = ['GENERAL', 'RISK', 'FILTER', 'INDICATOR', 'MEAN_REVERSION', 'DAY_TRADE']
|
||||
|
||||
// ── 개별 파라미터 행 컴포넌트 ─────────────────────────────────────────────────
|
||||
|
||||
interface ConfigRowProps {
|
||||
cfg: StrategyConfig
|
||||
onSave: (key: string, value: string) => void
|
||||
onToggle: (key: string) => void
|
||||
onReset: (key: string) => void
|
||||
isSaving: boolean
|
||||
}
|
||||
|
||||
function ConfigRow({ cfg, onSave, onToggle, onReset, isSaving }: ConfigRowProps) {
|
||||
const [editValue, setEditValue] = useState(cfg.value)
|
||||
const [dirty, setDirty] = useState(false)
|
||||
|
||||
const isDefault = cfg.value === cfg.default_value
|
||||
const isModified = !isDefault && !dirty
|
||||
|
||||
const handleChange = (val: string) => {
|
||||
setEditValue(val)
|
||||
setDirty(val !== cfg.value)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(cfg.key, editValue)
|
||||
setDirty(false)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setEditValue(cfg.default_value)
|
||||
setDirty(cfg.default_value !== cfg.value)
|
||||
onReset(cfg.key)
|
||||
}
|
||||
|
||||
const isBool = cfg.value_type === 'bool'
|
||||
const isTrue = cfg.value === 'true' || cfg.value === 'True' || cfg.value === '1'
|
||||
|
||||
return (
|
||||
<div className={`relative bg-slate-900 border rounded-lg p-3 transition-all ${
|
||||
!cfg.is_active
|
||||
? 'border-slate-800 opacity-40'
|
||||
: isModified
|
||||
? 'border-yellow-500/40 bg-yellow-500/5'
|
||||
: dirty
|
||||
? 'border-blue-500/40 bg-blue-500/5'
|
||||
: 'border-slate-800 hover:border-slate-700'
|
||||
}`}>
|
||||
{/* 상태 뱃지 */}
|
||||
{(isModified || dirty) && (
|
||||
<span className={`absolute top-1.5 right-1.5 text-[9px] px-1 py-0.5 rounded font-medium ${
|
||||
dirty ? 'bg-blue-500/20 text-blue-400' : 'bg-yellow-500/20 text-yellow-400'
|
||||
}`}>
|
||||
{dirty ? '미저장' : '수정됨'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 레이블 + 설명 */}
|
||||
<p className="text-xs font-medium text-slate-200 pr-8 leading-snug">{cfg.label}</p>
|
||||
{cfg.description && (
|
||||
<p className="text-[10px] text-slate-600 mt-0.5 mb-2 line-clamp-1">{cfg.description}</p>
|
||||
)}
|
||||
|
||||
{/* 컨트롤 영역 */}
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
{isBool ? (
|
||||
<button
|
||||
onClick={() => onSave(cfg.key, isTrue ? 'false' : 'true')}
|
||||
disabled={!cfg.is_active}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${isTrue ? 'bg-blue-500' : 'bg-slate-700'}`}
|
||||
>
|
||||
<span className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${isTrue ? 'translate-x-4' : 'translate-x-0.5'}`} />
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
type={cfg.value_type === 'int' || cfg.value_type === 'float' ? 'number' : 'text'}
|
||||
value={editValue}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
disabled={!cfg.is_active}
|
||||
step={cfg.value_type === 'float' ? '0.001' : '1'}
|
||||
min={cfg.min_value ?? undefined}
|
||||
max={cfg.max_value ?? undefined}
|
||||
className="w-20 px-2 py-1 text-xs bg-slate-800 border border-slate-700 rounded text-white text-right
|
||||
focus:outline-none focus:border-blue-500 disabled:opacity-40"
|
||||
/>
|
||||
<span className="text-[10px] text-slate-600">/ {cfg.default_value}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
{!isBool && (
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!dirty || isSaving}
|
||||
title="저장"
|
||||
className="p-1 rounded text-blue-400 hover:bg-blue-500/10 disabled:opacity-20 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Save size={11} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onToggle(cfg.key)}
|
||||
title={cfg.is_active ? '비활성화' : '활성화'}
|
||||
className={`p-1 rounded transition-colors ${cfg.is_active ? 'text-slate-400 hover:bg-slate-700' : 'text-slate-600 hover:bg-slate-700'}`}
|
||||
>
|
||||
{cfg.is_active ? <Power size={11} /> : <PowerOff size={11} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
title="기본값으로 초기화"
|
||||
className="p-1 rounded text-slate-500 hover:text-yellow-400 hover:bg-yellow-500/10 transition-colors"
|
||||
>
|
||||
<RotateCcw size={10} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 그룹 섹션 컴포넌트 ────────────────────────────────────────────────────────
|
||||
|
||||
const GROUP_ICONS: Record<string, string> = {
|
||||
GENERAL: '⚙️',
|
||||
RISK: '🛡️',
|
||||
FILTER: '🔍',
|
||||
INDICATOR: '📊',
|
||||
MEAN_REVERSION: '↩️',
|
||||
DAY_TRADE: '⚡',
|
||||
}
|
||||
|
||||
interface GroupSectionProps {
|
||||
groupKey: string
|
||||
configs: StrategyConfig[]
|
||||
onSave: (key: string, value: string) => void
|
||||
onToggle: (key: string) => void
|
||||
onReset: (key: string) => void
|
||||
isSaving: boolean
|
||||
}
|
||||
|
||||
function GroupSection({ groupKey, configs, onSave, onToggle, onReset, isSaving }: GroupSectionProps) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const modifiedCount = configs.filter(c => c.value !== c.default_value).length
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 그룹 헤더 */}
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="w-full flex items-center justify-between py-2 mb-3 group"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{GROUP_ICONS[groupKey] || '📁'}</span>
|
||||
<span className="font-semibold text-slate-200 text-sm">
|
||||
{GROUP_LABELS[groupKey] || groupKey}
|
||||
</span>
|
||||
<span className="text-xs text-slate-600">{configs.length}개</span>
|
||||
{modifiedCount > 0 && (
|
||||
<span className="text-xs px-2 py-0.5 bg-yellow-500/15 text-yellow-400 rounded-full">
|
||||
{modifiedCount}개 수정됨
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{collapsed
|
||||
? <ChevronRight size={15} className="text-slate-500" />
|
||||
: <ChevronDown size={15} className="text-slate-500" />
|
||||
}
|
||||
</button>
|
||||
|
||||
{/* 파라미터 카드 그리드 */}
|
||||
{!collapsed && (
|
||||
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||
{configs.map((cfg) => (
|
||||
<ConfigRow
|
||||
key={cfg.key}
|
||||
cfg={cfg}
|
||||
onSave={onSave}
|
||||
onToggle={onToggle}
|
||||
onReset={onReset}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 메인 페이지 ───────────────────────────────────────────────────────────────
|
||||
|
||||
const StrategySettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const [notification, setNotification] = useState<{ type: 'success' | 'error'; msg: string } | null>(null)
|
||||
|
||||
const showNotification = (type: 'success' | 'error', msg: string) => {
|
||||
setNotification({ type, msg })
|
||||
setTimeout(() => setNotification(null), 3000)
|
||||
}
|
||||
|
||||
// 전략 파라미터 목록 조회
|
||||
const { data: configs = [], isLoading, refetch } = useQuery<StrategyConfig[]>({
|
||||
queryKey: ['strategy-config'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/strategy-config')
|
||||
return res.data
|
||||
},
|
||||
staleTime: 10_000,
|
||||
})
|
||||
|
||||
// 단일 파라미터 수정
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async ({ key, value }: { key: string; value: string }) => {
|
||||
return api.put(`/strategy-config/${key}`, { value, updated_by: 'ui' })
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['strategy-config'] })
|
||||
showNotification('success', '저장 완료')
|
||||
},
|
||||
onError: (err: any) => {
|
||||
showNotification('error', `저장 실패: ${err.response?.data?.detail || err.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
// 활성화 토글
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: async (key: string) => {
|
||||
return api.patch(`/strategy-config/${key}/toggle`)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['strategy-config'] })
|
||||
},
|
||||
onError: (err: any) => {
|
||||
showNotification('error', `토글 실패: ${err.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
// 단일 기본값 리셋
|
||||
const resetOneMutation = useMutation({
|
||||
mutationFn: async (key: string) => {
|
||||
return api.post(`/strategy-config/${key}/reset`)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['strategy-config'] })
|
||||
showNotification('success', '기본값으로 초기화 완료')
|
||||
},
|
||||
})
|
||||
|
||||
// 전체 기본값 리셋
|
||||
const resetAllMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return api.post('/strategy-config/reset-all')
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['strategy-config'] })
|
||||
showNotification('success', '전체 파라미터가 기본값으로 초기화되었습니다.')
|
||||
},
|
||||
onError: (err: any) => {
|
||||
showNotification('error', `초기화 실패: ${err.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
// 그룹별 분류
|
||||
const grouped = useMemo(() => {
|
||||
const map: Record<string, StrategyConfig[]> = {}
|
||||
for (const cfg of configs) {
|
||||
if (!map[cfg.group]) map[cfg.group] = []
|
||||
map[cfg.group].push(cfg)
|
||||
}
|
||||
return map
|
||||
}, [configs])
|
||||
|
||||
// 수정된 파라미터 수
|
||||
const totalModified = configs.filter(c => c.value !== c.default_value).length
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<HtsPage>
|
||||
<div className="flex items-center justify-center h-64 text-slate-400">
|
||||
로딩 중...
|
||||
</div>
|
||||
</HtsPage>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<HtsPage>
|
||||
<div className="space-y-5">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">전략 설정</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
변경 즉시 캐시 무효화 → 30초 이내 전략 코드에 반영됩니다.
|
||||
비활성화 시 config.py 기본값이 사용됩니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-slate-400 hover:text-white border border-slate-700 rounded-lg hover:border-slate-500 transition-colors"
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
새로고침
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm('모든 파라미터를 config.py 기본값으로 초기화하시겠습니까?')) {
|
||||
resetAllMutation.mutate()
|
||||
}
|
||||
}}
|
||||
disabled={resetAllMutation.isPending || totalModified === 0}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-yellow-400 border border-yellow-500/40 rounded-lg hover:bg-yellow-500/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
전체 초기화 {totalModified > 0 && `(${totalModified})`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 알림 배너 */}
|
||||
{notification && (
|
||||
<div className={`px-4 py-2.5 rounded-lg text-sm font-medium ${
|
||||
notification.type === 'success'
|
||||
? 'bg-green-500/15 text-green-400 border border-green-500/30'
|
||||
: 'bg-red-500/15 text-red-400 border border-red-500/30'
|
||||
}`}>
|
||||
{notification.msg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 파라미터 없음 */}
|
||||
{configs.length === 0 && (
|
||||
<div className="text-center py-16 text-slate-500">
|
||||
<p>전략 파라미터가 없습니다.</p>
|
||||
<p className="text-xs mt-2">DB 마이그레이션 후 서버를 재시작하면 기본값이 자동 생성됩니다.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 그룹별 섹션 — 2열 그리드 */}
|
||||
<div className="grid grid-cols-3 gap-5 items-start">
|
||||
{[
|
||||
...GROUP_ORDER.filter(g => grouped[g]?.length),
|
||||
...Object.keys(grouped).filter(g => !GROUP_ORDER.includes(g) && grouped[g]?.length),
|
||||
].map(groupKey => (
|
||||
<div key={groupKey} className="bg-slate-900/40 border border-slate-800 rounded-xl p-4">
|
||||
<GroupSection
|
||||
groupKey={groupKey}
|
||||
configs={grouped[groupKey]}
|
||||
onSave={(key, value) => updateMutation.mutate({ key, value })}
|
||||
onToggle={(key) => toggleMutation.mutate(key)}
|
||||
onReset={(key) => resetOneMutation.mutate(key)}
|
||||
isSaving={updateMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</HtsPage>
|
||||
)
|
||||
}
|
||||
|
||||
export default StrategySettings
|
||||
Reference in New Issue
Block a user