백테스팅 목록 아이템 삭제 기능추가
This commit is contained in:
@@ -75,6 +75,15 @@ public class PaperTradingController {
|
|||||||
uid, symbol, side, source, from, to, page, pageSize));
|
uid, symbol, side, source, from, to, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/trades/batch")
|
||||||
|
public ResponseEntity<Map<String, Integer>> deleteTradesBatch(
|
||||||
|
@RequestHeader Map<String, String> h,
|
||||||
|
@RequestBody PaperTradeDeleteRequest body) {
|
||||||
|
long uid = TradingControllerSupport.requireRegisteredUser(h);
|
||||||
|
int deleted = paperTradingService.deleteTradesBatch(uid, body != null ? body.getIds() : null);
|
||||||
|
return ResponseEntity.ok(Map.of("deleted", deleted));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/ledger")
|
@GetMapping("/ledger")
|
||||||
public ResponseEntity<PaperPageDto<PaperLedgerEntryDto>> ledger(
|
public ResponseEntity<PaperPageDto<PaperLedgerEntryDto>> ledger(
|
||||||
@RequestHeader Map<String, String> h,
|
@RequestHeader Map<String, String> h,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.goldenchart.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PaperTradeDeleteRequest {
|
||||||
|
private List<Long> ids;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import org.springframework.data.jpa.repository.Query;
|
|||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface GcPaperTradeRepository extends JpaRepository<GcPaperTrade, Long> {
|
public interface GcPaperTradeRepository extends JpaRepository<GcPaperTrade, Long> {
|
||||||
@@ -30,4 +31,6 @@ public interface GcPaperTradeRepository extends JpaRepository<GcPaperTrade, Long
|
|||||||
@Param("from") LocalDateTime from,
|
@Param("from") LocalDateTime from,
|
||||||
@Param("to") LocalDateTime to,
|
@Param("to") LocalDateTime to,
|
||||||
Pageable pageable);
|
Pageable pageable);
|
||||||
|
|
||||||
|
List<GcPaperTrade> findByAccountIdAndIdIn(Long accountId, Collection<Long> ids);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,21 @@ public class PaperTradingService {
|
|||||||
return listTrades(userId, null, null, null, null, null, 0, 100).getContent();
|
return listTrades(userId, null, null, null, null, null, 0, 100).getContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 체결 이력 일괄 삭제 (분석 목록에서 그룹 제거용) */
|
||||||
|
@Transactional
|
||||||
|
public int deleteTradesBatch(Long userId, List<Long> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) return 0;
|
||||||
|
long uid = TradingAccess.requireUserId(userId);
|
||||||
|
GcAppSettings app = appSettingsService.getEntity(uid, null);
|
||||||
|
GcPaperAccount account = getOrCreateAccount(uid, app);
|
||||||
|
List<Long> distinct = ids.stream().filter(id -> id != null && id > 0).distinct().toList();
|
||||||
|
if (distinct.isEmpty()) return 0;
|
||||||
|
List<GcPaperTrade> owned = tradeRepo.findByAccountIdAndIdIn(account.getId(), distinct);
|
||||||
|
if (owned.isEmpty()) return 0;
|
||||||
|
tradeRepo.deleteAll(owned);
|
||||||
|
return owned.size();
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public PaperPageDto<PaperLedgerEntryDto> listLedger(Long userId,
|
public PaperPageDto<PaperLedgerEntryDto> listLedger(Long userId,
|
||||||
LocalDateTime from, LocalDateTime to,
|
LocalDateTime from, LocalDateTime to,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { MarketSearchPanel } from './MarketSearchPanel';
|
|||||||
import {
|
import {
|
||||||
loadBacktestResults,
|
loadBacktestResults,
|
||||||
deleteBacktestResult,
|
deleteBacktestResult,
|
||||||
|
deletePaperTradesBatch,
|
||||||
loadPaperTrades,
|
loadPaperTrades,
|
||||||
loadPaperSummary,
|
loadPaperSummary,
|
||||||
loadStrategies,
|
loadStrategies,
|
||||||
@@ -41,7 +42,7 @@ import {
|
|||||||
buildLiveReportModel,
|
buildLiveReportModel,
|
||||||
} from '../utils/backtestReportModel';
|
} from '../utils/backtestReportModel';
|
||||||
import { buildLiveExecutionItems, paperTradesToSignals, type LiveExecutionItem } from '../utils/liveExecutionGroups';
|
import { buildLiveExecutionItems, paperTradesToSignals, type LiveExecutionItem } from '../utils/liveExecutionGroups';
|
||||||
import { PAPER_TRADES_CHANGED_EVENT } from '../utils/paperTradeEvents';
|
import { PAPER_TRADES_CHANGED_EVENT, notifyPaperTradesChanged } from '../utils/paperTradeEvents';
|
||||||
import {
|
import {
|
||||||
readStoredSize,
|
readStoredSize,
|
||||||
storeSize,
|
storeSize,
|
||||||
@@ -222,6 +223,42 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
await fetchAll();
|
await fetchAll();
|
||||||
}, [records, fetchAll]);
|
}, [records, fetchAll]);
|
||||||
|
|
||||||
|
const handleDeleteBacktest = useCallback(async (record: BacktestResultRecord) => {
|
||||||
|
if (record.id == null) return;
|
||||||
|
const market = toUpbitMarket(record.symbol);
|
||||||
|
const ko = getKoreanName(market);
|
||||||
|
if (!window.confirm(`"${ko}" 백테스팅 결과를 삭제하시겠습니까?`)) return;
|
||||||
|
try {
|
||||||
|
await deleteBacktestResult(record.id);
|
||||||
|
setRecords(prev => {
|
||||||
|
const next = prev.filter(r => r.id !== record.id);
|
||||||
|
setSelectedBacktest(sel => (sel?.id === record.id ? next[0] ?? null : sel));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
window.alert(e instanceof Error ? e.message : '삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteLive = useCallback(async (item: LiveExecutionItem) => {
|
||||||
|
const ids = item.trades.map(t => t.id).filter(id => id > 0);
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
const market = toUpbitMarket(item.symbol);
|
||||||
|
const ko = getKoreanName(market);
|
||||||
|
if (!window.confirm(`"${ko}" ${item.strategyLabel} 실시간 매매 이력(${ids.length}건)을 삭제하시겠습니까?`)) return;
|
||||||
|
try {
|
||||||
|
const deleted = await deletePaperTradesBatch(ids);
|
||||||
|
if (deleted <= 0) {
|
||||||
|
window.alert('삭제할 체결 이력을 찾지 못했습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await refreshLiveTrades();
|
||||||
|
notifyPaperTradesChanged();
|
||||||
|
} catch (e) {
|
||||||
|
window.alert(e instanceof Error ? e.message : '삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}, [refreshLiveTrades]);
|
||||||
|
|
||||||
const handleQuickRun = useCallback(async () => {
|
const handleQuickRun = useCallback(async () => {
|
||||||
if (!runStrategyId) {
|
if (!runStrategyId) {
|
||||||
window.alert('전략을 선택해주세요.');
|
window.alert('전략을 선택해주세요.');
|
||||||
@@ -519,6 +556,8 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
|||||||
selectedLiveId={selectedLive?.id ?? null}
|
selectedLiveId={selectedLive?.id ?? null}
|
||||||
onSelectBacktest={r => { setSelectedBacktest(r); setTab('backtest'); }}
|
onSelectBacktest={r => { setSelectedBacktest(r); setTab('backtest'); }}
|
||||||
onSelectLive={item => { setSelectedLive(item); setTab('live'); }}
|
onSelectLive={item => { setSelectedLive(item); setTab('live'); }}
|
||||||
|
onDeleteBacktest={r => void handleDeleteBacktest(r)}
|
||||||
|
onDeleteLive={item => void handleDeleteLive(item)}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import type { Theme, Timeframe } from '../../types';
|
import type { Theme, Timeframe } from '../../types';
|
||||||
import {
|
import {
|
||||||
loadBacktestResults,
|
loadBacktestResults,
|
||||||
|
deleteBacktestResult,
|
||||||
|
deletePaperTradesBatch,
|
||||||
loadPaperTrades,
|
loadPaperTrades,
|
||||||
loadPaperSummary,
|
loadPaperSummary,
|
||||||
loadStrategies,
|
loadStrategies,
|
||||||
@@ -16,7 +18,7 @@ import {
|
|||||||
buildContextFromLive,
|
buildContextFromLive,
|
||||||
} from '../../utils/analysisReportContext';
|
} from '../../utils/analysisReportContext';
|
||||||
import { buildLiveExecutionItems, type LiveExecutionItem } from '../../utils/liveExecutionGroups';
|
import { buildLiveExecutionItems, type LiveExecutionItem } from '../../utils/liveExecutionGroups';
|
||||||
import { PAPER_TRADES_CHANGED_EVENT } from '../../utils/paperTradeEvents';
|
import { PAPER_TRADES_CHANGED_EVENT, notifyPaperTradesChanged } from '../../utils/paperTradeEvents';
|
||||||
import BacktestExecutionList, { type ExecutionListTab } from '../backtest/BacktestExecutionList';
|
import BacktestExecutionList, { type ExecutionListTab } from '../backtest/BacktestExecutionList';
|
||||||
import BuilderPageShell from '../layout/BuilderPageShell';
|
import BuilderPageShell from '../layout/BuilderPageShell';
|
||||||
import AnalysisReportCenterPanel from './AnalysisReportCenterPanel';
|
import AnalysisReportCenterPanel from './AnalysisReportCenterPanel';
|
||||||
@@ -108,6 +110,38 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
|
|||||||
return () => window.removeEventListener(PAPER_TRADES_CHANGED_EVENT, onPaper);
|
return () => window.removeEventListener(PAPER_TRADES_CHANGED_EVENT, onPaper);
|
||||||
}, [refreshLive, records]);
|
}, [refreshLive, records]);
|
||||||
|
|
||||||
|
const handleDeleteBacktest = useCallback(async (record: BacktestResultRecord) => {
|
||||||
|
if (record.id == null) return;
|
||||||
|
if (!window.confirm('이 백테스팅 결과를 삭제하시겠습니까?')) return;
|
||||||
|
try {
|
||||||
|
await deleteBacktestResult(record.id);
|
||||||
|
setRecords(prev => {
|
||||||
|
const next = prev.filter(r => r.id !== record.id);
|
||||||
|
setSelectedBacktest(sel => (sel?.id === record.id ? next[0] ?? null : sel));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
window.alert(e instanceof Error ? e.message : '삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteLive = useCallback(async (item: LiveExecutionItem) => {
|
||||||
|
const ids = item.trades.map(t => t.id).filter(id => id > 0);
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
if (!window.confirm(`실시간 매매 이력 ${ids.length}건을 삭제하시겠습니까?`)) return;
|
||||||
|
try {
|
||||||
|
const deleted = await deletePaperTradesBatch(ids);
|
||||||
|
if (deleted <= 0) {
|
||||||
|
window.alert('삭제할 체결 이력을 찾지 못했습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await refreshLive(records);
|
||||||
|
notifyPaperTradesChanged();
|
||||||
|
} catch (e) {
|
||||||
|
window.alert(e instanceof Error ? e.message : '삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}, [refreshLive, records]);
|
||||||
|
|
||||||
const compareBacktest = useMemo(() => {
|
const compareBacktest = useMemo(() => {
|
||||||
if (!selectedBacktest) return null;
|
if (!selectedBacktest) return null;
|
||||||
return pickCompareBacktest(selectedBacktest, records);
|
return pickCompareBacktest(selectedBacktest, records);
|
||||||
@@ -228,6 +262,8 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
|
|||||||
setSelectedLive(item);
|
setSelectedLive(item);
|
||||||
setSourceTab('live');
|
setSourceTab('live');
|
||||||
}}
|
}}
|
||||||
|
onDeleteBacktest={r => void handleDeleteBacktest(r)}
|
||||||
|
onDeleteLive={item => void handleDeleteLive(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
center={(
|
center={(
|
||||||
|
|||||||
@@ -29,8 +29,19 @@ interface Props {
|
|||||||
selectedLiveId: string | null;
|
selectedLiveId: string | null;
|
||||||
onSelectBacktest: (r: BacktestResultRecord) => void;
|
onSelectBacktest: (r: BacktestResultRecord) => void;
|
||||||
onSelectLive: (item: LiveExecutionItem) => void;
|
onSelectLive: (item: LiveExecutionItem) => void;
|
||||||
|
onDeleteBacktest?: (r: BacktestResultRecord) => void;
|
||||||
|
onDeleteLive?: (item: LiveExecutionItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IcTrash = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17" />
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
function parseBacktestSpark(r: BacktestResultRecord) {
|
function parseBacktestSpark(r: BacktestResultRecord) {
|
||||||
try {
|
try {
|
||||||
const signals = r.signalsJson ? JSON.parse(r.signalsJson) : [];
|
const signals = r.signalsJson ? JSON.parse(r.signalsJson) : [];
|
||||||
@@ -55,6 +66,8 @@ export default function BacktestExecutionList({
|
|||||||
selectedLiveId,
|
selectedLiveId,
|
||||||
onSelectBacktest,
|
onSelectBacktest,
|
||||||
onSelectLive,
|
onSelectLive,
|
||||||
|
onDeleteBacktest,
|
||||||
|
onDeleteLive,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [sort, setSort] = useState<BacktestListSort>('return');
|
const [sort, setSort] = useState<BacktestListSort>('return');
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
@@ -197,20 +210,36 @@ export default function BacktestExecutionList({
|
|||||||
const ko = getKoreanName(market);
|
const ko = getKoreanName(market);
|
||||||
const strategy = r.strategyName || '전략 없음';
|
const strategy = r.strategyName || '전략 없음';
|
||||||
return (
|
return (
|
||||||
<button type="button" className={`btd-history-card${active ? ' btd-history-card--active' : ''}`} onClick={() => onSelectBacktest(r)}>
|
<div className={`btd-history-card${active ? ' btd-history-card--active' : ''}`}>
|
||||||
<div className="btd-history-card-row">
|
<button type="button" className="btd-history-card-body" onClick={() => onSelectBacktest(r)}>
|
||||||
<div className="btd-history-card-main">
|
<div className="btd-history-card-row">
|
||||||
<span className="btd-history-ko">{ko}</span>
|
<div className="btd-history-card-main">
|
||||||
<span className="btd-history-strategy">{strategy} · {formatTimeframeKo(resolveBacktestRecordExecTimeframe(r))}</span>
|
<span className="btd-history-ko">{ko}</span>
|
||||||
<span className="btd-history-date">{fmtListTimestamp(r.createdAt)}</span>
|
<span className="btd-history-strategy">{strategy} · {formatTimeframeKo(resolveBacktestRecordExecTimeframe(r))}</span>
|
||||||
|
<span className="btd-history-date">{fmtListTimestamp(r.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="btd-history-card-side">
|
||||||
|
<BacktestSparkline curve={parseBacktestSpark(r)} positive={positive} width={56} height={22} />
|
||||||
|
<span className="btd-history-win">승률 {pctAbs(r.winRate ?? 0)}</span>
|
||||||
|
<span className={`btd-history-roi${positive ? ' up' : ' down'}`}>총 수익률 {pct(ret)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="btd-history-card-side">
|
</button>
|
||||||
<BacktestSparkline curve={parseBacktestSpark(r)} positive={positive} width={56} height={22} />
|
{onDeleteBacktest && r.id != null && (
|
||||||
<span className="btd-history-win">승률 {pctAbs(r.winRate ?? 0)}</span>
|
<button
|
||||||
<span className={`btd-history-roi${positive ? ' up' : ' down'}`}>총 수익률 {pct(ret)}</span>
|
type="button"
|
||||||
</div>
|
className="btd-history-card-delete"
|
||||||
</div>
|
title="백테스팅 결과 삭제"
|
||||||
</button>
|
aria-label="백테스팅 결과 삭제"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteBacktest(r);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IcTrash />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</VirtualScroll>
|
</VirtualScroll>
|
||||||
@@ -241,24 +270,40 @@ export default function BacktestExecutionList({
|
|||||||
const market = toUpbitMarket(item.symbol);
|
const market = toUpbitMarket(item.symbol);
|
||||||
const ko = getKoreanName(market);
|
const ko = getKoreanName(market);
|
||||||
return (
|
return (
|
||||||
<button type="button" className={`btd-history-card${active ? ' btd-history-card--active' : ''}`} onClick={() => onSelectLive(item)}>
|
<div className={`btd-history-card${active ? ' btd-history-card--active' : ''}`}>
|
||||||
<div className="btd-history-card-row">
|
<button type="button" className="btd-history-card-body" onClick={() => onSelectLive(item)}>
|
||||||
<div className="btd-history-card-main">
|
<div className="btd-history-card-row">
|
||||||
<span className="btd-history-ko">{ko}</span>
|
<div className="btd-history-card-main">
|
||||||
<span className="btd-history-strategy">
|
<span className="btd-history-ko">{ko}</span>
|
||||||
{item.strategyLabel}
|
<span className="btd-history-strategy">
|
||||||
{item.timeframe !== 'unknown' ? ` · ${formatTimeframeKo(item.timeframe)}` : ''}
|
{item.strategyLabel}
|
||||||
{item.sourceLabel !== '자동' ? ` · ${item.sourceLabel}` : ''}
|
{item.timeframe !== 'unknown' ? ` · ${formatTimeframeKo(item.timeframe)}` : ''}
|
||||||
</span>
|
{item.sourceLabel !== '자동' ? ` · ${item.sourceLabel}` : ''}
|
||||||
<span className="btd-history-date">{fmtShortTimestamp(item.createdAt)}</span>
|
</span>
|
||||||
|
<span className="btd-history-date">{fmtShortTimestamp(item.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="btd-history-card-side">
|
||||||
|
<BacktestSparkline curve={parseLiveSpark(item)} positive={positive} width={56} height={22} />
|
||||||
|
<span className="btd-history-win">라운드 {item.roundTripCount} · 체결 {item.tradeCount}건</span>
|
||||||
|
<span className={`btd-history-roi${positive ? ' up' : ' down'}`}>{pct(item.totalReturnPct)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="btd-history-card-side">
|
</button>
|
||||||
<BacktestSparkline curve={parseLiveSpark(item)} positive={positive} width={56} height={22} />
|
{onDeleteLive && item.trades.some(t => t.id > 0) && (
|
||||||
<span className="btd-history-win">라운드 {item.roundTripCount} · 체결 {item.tradeCount}건</span>
|
<button
|
||||||
<span className={`btd-history-roi${positive ? ' up' : ' down'}`}>{pct(item.totalReturnPct)}</span>
|
type="button"
|
||||||
</div>
|
className="btd-history-card-delete"
|
||||||
</div>
|
title="실시간 매매 이력 삭제"
|
||||||
</button>
|
aria-label="실시간 매매 이력 삭제"
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteLive(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IcTrash />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</VirtualScroll>
|
</VirtualScroll>
|
||||||
|
|||||||
@@ -1353,16 +1353,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btd-history-card {
|
.btd-history-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 2px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: 1px solid var(--btd-divider);
|
border: 1px solid var(--btd-divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--btd-surface);
|
background: var(--btd-surface);
|
||||||
padding: 10px;
|
padding: 0;
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btd-history-card-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-history-card-delete {
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-right: 6px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--btd-text-muted, #787b86);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.72;
|
||||||
|
transition: color 0.15s, background 0.15s, opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-history-card:hover .btd-history-card-delete,
|
||||||
|
.btd-history-card--active .btd-history-card-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-history-card-delete:hover {
|
||||||
|
color: var(--btd-bear, #ef5350);
|
||||||
|
background: color-mix(in srgb, var(--btd-bear, #ef5350) 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.btd-history-card:hover {
|
.btd-history-card:hover {
|
||||||
border-color: color-mix(in srgb, var(--btd-gold) 35%, var(--btd-divider));
|
border-color: color-mix(in srgb, var(--btd-gold) 35%, var(--btd-divider));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1348,6 +1348,16 @@ export async function deleteBacktestResult(id: number): Promise<void> {
|
|||||||
await request(`/backtest-results/${id}`, { method: 'DELETE' });
|
await request(`/backtest-results/${id}`, { method: 'DELETE' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 모의·가상투자 체결 이력 일괄 삭제 (실시간 매매 목록 그룹 제거) */
|
||||||
|
export async function deletePaperTradesBatch(ids: number[]): Promise<number> {
|
||||||
|
if (ids.length === 0) return 0;
|
||||||
|
const res = await request<{ deleted?: number }>('/paper/trades/batch', {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: JSON.stringify({ ids }),
|
||||||
|
});
|
||||||
|
return res?.deleted ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// 실시간 전략 체크 설정 API
|
// 실시간 전략 체크 설정 API
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user