수정본
This commit is contained in:
@@ -1,660 +1,18 @@
|
||||
/* HTS 대시보드 컨테이너 */
|
||||
.hts-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #000000;
|
||||
color: #e0e6ed;
|
||||
font-family: 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 메인 레이아웃 */
|
||||
.hts-main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr 420px;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 왼쪽 패널: 계좌 정보 & 보유종목 */
|
||||
.left-panel {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px 15px;
|
||||
background: #222;
|
||||
border-bottom: 1px solid #333;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
padding: 12px 15px;
|
||||
background: #111;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.balance-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.balance-row span:first-child {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.balance-row span.up {
|
||||
color: #ff3b3b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.balance-row span.down {
|
||||
color: #2979ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.portfolio-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 6px;
|
||||
background: #222;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
background: #2a2a2a;
|
||||
border-color: #4fc3f7;
|
||||
}
|
||||
|
||||
.portfolio-item .stock-name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.portfolio-item .stock-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.portfolio-item .stock-info span.up {
|
||||
color: #ff3b3b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.portfolio-item .stock-info span.down {
|
||||
color: #2979ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 중앙 패널 */
|
||||
.center-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 차트 섹션 */
|
||||
.chart-section {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 420px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chart-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chart-stocks {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chart-stock {
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 120px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chart-stock .name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.chart-stock .rate {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chart-stock.active {
|
||||
border-color: #00ff88;
|
||||
box-shadow: 0 0 12px rgba(0, 255, 136, 0.2);
|
||||
}
|
||||
|
||||
.chart-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ws-status {
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: rgba(136, 146, 176, 0.2);
|
||||
border: 1px solid rgba(136, 146, 176, 0.4);
|
||||
}
|
||||
|
||||
.ws-status.online {
|
||||
background: rgba(0, 255, 136, 0.15);
|
||||
border-color: rgba(0, 255, 136, 0.5);
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.ws-status.offline {
|
||||
color: #8892b0;
|
||||
}
|
||||
|
||||
.chart-period {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chart-period button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #333;
|
||||
background: #111;
|
||||
color: #8892b0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chart-period button.active,
|
||||
.chart-period button:hover {
|
||||
border-color: #00ff88;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
min-height: 340px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #222;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 거래 이력 섹션 */
|
||||
.trades-section {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 280px;
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.trades-table-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.trades-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.trades-table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #1a1a1a;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.trades-table th {
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.trades-table td {
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
color: #ccc;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
|
||||
.trades-table tbody tr:hover {
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.trade-status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.trade-status.executed {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.trade-status.pending {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.trade-status.failed {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* 오른쪽 패널 */
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.recommend-section,
|
||||
.mcp-status-section,
|
||||
.news-section,
|
||||
.ai-analysis-section,
|
||||
.risk-check-section {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ai-analysis-section {
|
||||
min-height: 180px;
|
||||
max-height: 220px;
|
||||
}
|
||||
|
||||
.risk-check-section {
|
||||
min-height: 150px;
|
||||
max-height: 180px;
|
||||
}
|
||||
|
||||
.analysis-content,
|
||||
.risk-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #222;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.analysis-loading,
|
||||
.risk-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.risk-item {
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.risk-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.risk-level {
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.risk-level.low {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.risk-level.medium {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.risk-level.high {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.risk-description {
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.recommend-section {
|
||||
min-height: 300px;
|
||||
max-height: 360px;
|
||||
}
|
||||
|
||||
.recommend-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.recommend-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.recommend-item .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.recommend-item .info .name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.recommend-item .info .code {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.recommend-item .figures {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.recommend-item .figures .price {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.recommend-item .figures .rate {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.recommend-item:hover,
|
||||
.recommend-item.active {
|
||||
border-color: #00ff88;
|
||||
box-shadow: 0 0 12px rgba(0, 255, 136, 0.2);
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
color: #4fc3f7;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
background: #222;
|
||||
border-color: #4fc3f7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4fc3f7;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #6fd4ff;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #2a4a5a;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mcp-status-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mcp-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: #222;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.connected {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.status-indicator.disconnected {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mcp-tools-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mcp-tool-badge {
|
||||
padding: 6px;
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.news-section {
|
||||
flex: 1;
|
||||
min-height: 180px;
|
||||
max-height: 220px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.news-section .news-list {
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.news-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.news-item {
|
||||
padding: 10px;
|
||||
background: #222;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.news-item:hover {
|
||||
border-color: #4fc3f7;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 공통 */
|
||||
.no-data {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 색상 */
|
||||
.up {
|
||||
color: #ff3b3b !important;
|
||||
}
|
||||
|
||||
.down {
|
||||
color: #2979ff !important;
|
||||
}
|
||||
|
||||
/* 스크롤바 */
|
||||
::-webkit-scrollbar {
|
||||
/* Custom Scrollbar for Dashboard containers */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #374151; /* gray-700 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #444;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #4b5563; /* gray-600 */
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import api from '../lib/api'
|
||||
interface Trade {
|
||||
id: string
|
||||
created_at: string
|
||||
executed_at?: string
|
||||
status: 'EXECUTED' | 'FAILED' | 'PENDING' | string
|
||||
stock_code: string
|
||||
stock_name?: string
|
||||
@@ -16,7 +17,13 @@ interface Trade {
|
||||
order_quantity: number
|
||||
executed_price: number | null
|
||||
executed_quantity: number | null
|
||||
total_amount?: number | null
|
||||
message?: string | null
|
||||
entry_reason?: string | null
|
||||
exit_reason?: string | null
|
||||
signals?: string[] | null
|
||||
profit_loss?: number | null
|
||||
profit_loss_rate?: number | null
|
||||
}
|
||||
|
||||
const TradeHistory = () => {
|
||||
@@ -82,11 +89,11 @@ const TradeHistory = () => {
|
||||
<th className="text-left py-3 px-4 text-slate-400">상태</th>
|
||||
<th className="text-left py-3 px-4 text-slate-400">종목</th>
|
||||
<th className="text-center py-3 px-4 text-slate-400">구분</th>
|
||||
<th className="text-right py-3 px-4 text-slate-400">주문가</th>
|
||||
<th className="text-right py-3 px-4 text-slate-400">가격</th>
|
||||
<th className="text-right py-3 px-4 text-slate-400">수량</th>
|
||||
<th className="text-right py-3 px-4 text-slate-400">체결가</th>
|
||||
<th className="text-right py-3 px-4 text-slate-400">체결수량</th>
|
||||
<th className="text-left py-3 px-4 text-slate-400">메시지</th>
|
||||
<th className="text-right py-3 px-4 text-slate-400">총액</th>
|
||||
<th className="text-right py-3 px-4 text-slate-400">손익</th>
|
||||
<th className="text-left py-3 px-4 text-slate-400">이유/신호</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -130,19 +137,55 @@ const TradeHistory = () => {
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right text-white">
|
||||
{formatCurrency(trade.order_price)}
|
||||
{formatCurrency(trade.executed_price || trade.order_price)}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right text-white">
|
||||
{trade.order_quantity.toLocaleString()}
|
||||
{(trade.executed_quantity || trade.order_quantity).toLocaleString()}주
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right text-white">
|
||||
{trade.executed_price != null ? formatCurrency(trade.executed_price) : '-'}
|
||||
<td className="py-3 px-4 text-right text-white font-medium">
|
||||
{trade.total_amount != null ? formatCurrency(trade.total_amount) : '-'}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right text-white">
|
||||
{trade.executed_quantity != null ? trade.executed_quantity.toLocaleString() : '-'}
|
||||
<td className="py-3 px-4 text-right">
|
||||
{trade.order_type === 'SELL' && trade.profit_loss != null ? (
|
||||
<div className="space-y-0.5">
|
||||
<div className={`font-medium ${Number(trade.profit_loss) >= 0 ? 'text-primary' : 'text-danger'
|
||||
}`}>
|
||||
{Number(trade.profit_loss) >= 0 ? '+' : ''}{formatCurrency(Number(trade.profit_loss))}
|
||||
</div>
|
||||
{trade.profit_loss_rate != null && (
|
||||
<div className={`text-xs ${Number(trade.profit_loss_rate) >= 0 ? 'text-primary' : 'text-danger'
|
||||
}`}>
|
||||
({Number(trade.profit_loss_rate) >= 0 ? '+' : ''}{Number(trade.profit_loss_rate).toFixed(2)}%)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-500">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-slate-400 text-sm">
|
||||
{trade.message || '-'}
|
||||
<td className="py-3 px-4 text-slate-400 text-sm max-w-md">
|
||||
{trade.order_type === 'BUY' && trade.entry_reason && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-primary text-xs font-medium">📈 진입 이유</div>
|
||||
<div>{trade.entry_reason}</div>
|
||||
{trade.signals && trade.signals.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{trade.signals.slice(0, 3).map((signal, idx) => (
|
||||
<span key={idx} className="text-xs bg-slate-700 px-2 py-0.5 rounded">
|
||||
{signal}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{trade.order_type === 'SELL' && trade.exit_reason && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-danger text-xs font-medium">📉 매도 이유</div>
|
||||
<div>{trade.exit_reason}</div>
|
||||
</div>
|
||||
)}
|
||||
{!trade.entry_reason && !trade.exit_reason && (trade.message || '-')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -11,9 +11,10 @@ const TradingRules = () => {
|
||||
name: '',
|
||||
description: '',
|
||||
rule_type: 'BUY',
|
||||
priority: 1,
|
||||
conditions: '{\n "rsi_threshold": 40,\n "sma_short": 5,\n "sma_long": 20\n}',
|
||||
actions: '{\n "take_profit": 5.0,\n "stop_loss": 3.0,\n "position_sizing": {\n "target_ratio": 0.15\n }\n}'
|
||||
risk_level: 'MEDIUM',
|
||||
stop_loss: 3.0,
|
||||
take_profit: 5.0,
|
||||
max_quantity: 1
|
||||
})
|
||||
|
||||
// 트레이딩 규칙 조회
|
||||
@@ -28,36 +29,27 @@ const TradingRules = () => {
|
||||
// 규칙 생성
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (data: any) => {
|
||||
// MCP로 규칙 검증
|
||||
try {
|
||||
const validateResponse = await fetch('http://localhost:8001/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
analysis_type: 'RULE_VALIDATION',
|
||||
data: {
|
||||
name: data.name,
|
||||
conditions: JSON.parse(data.conditions),
|
||||
actions: JSON.parse(data.actions)
|
||||
}
|
||||
})
|
||||
});
|
||||
const validation = await validateResponse.json();
|
||||
// AI 자동 매매를 위한 기본 템플릿 구성
|
||||
const finalConditions = {
|
||||
type: "AI_AUTO_OPTIMIZATION",
|
||||
risk_level: data.risk_level,
|
||||
description: "AI가 시장 상황에 따라 진입 조건을 동적으로 결정합니다."
|
||||
};
|
||||
|
||||
if (!confirm(`🤖 AI 규칙 검증 결과:\n\n${validation.analysis || validation.result}\n\n계속 진행하시겠습니까?`)) {
|
||||
throw new Error('사용자가 취소했습니다.');
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.message === '사용자가 취소했습니다.') {
|
||||
throw error;
|
||||
}
|
||||
console.warn('MCP 검증 실패, 그대로 진행:', error);
|
||||
}
|
||||
const finalActions = {
|
||||
stop_loss: parseFloat(data.stop_loss),
|
||||
take_profit: parseFloat(data.take_profit),
|
||||
max_quantity: parseInt(data.max_quantity),
|
||||
trailing_stop: true
|
||||
};
|
||||
|
||||
return api.post('/rules', {
|
||||
...data,
|
||||
conditions: JSON.parse(data.conditions),
|
||||
actions: JSON.parse(data.actions)
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
rule_type: data.rule_type,
|
||||
priority: 1,
|
||||
conditions: finalConditions,
|
||||
actions: finalActions
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -67,16 +59,15 @@ const TradingRules = () => {
|
||||
name: '',
|
||||
description: '',
|
||||
rule_type: 'BUY',
|
||||
priority: 1,
|
||||
conditions: '{\n "rsi_threshold": 40,\n "sma_short": 5,\n "sma_long": 20\n}',
|
||||
actions: '{\n "take_profit": 5.0,\n "stop_loss": 3.0,\n "position_sizing": {\n "target_ratio": 0.15\n }\n}'
|
||||
risk_level: 'MEDIUM',
|
||||
stop_loss: 3.0,
|
||||
take_profit: 5.0,
|
||||
max_quantity: 1
|
||||
})
|
||||
alert('✅ 규칙이 성공적으로 추가되었습니다!')
|
||||
alert('✅ AI 자동매매 규칙이 설정되었습니다!')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
if (error.message !== '사용자가 취소했습니다.') {
|
||||
alert(`❌ 규칙 추가 실패: ${error.response?.data?.detail || error.message}`)
|
||||
}
|
||||
alert(`❌ 규칙 추가 실패: ${error.response?.data?.detail || error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -158,35 +149,50 @@ const TradingRules = () => {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1">우선순위</label>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1">리스크 레벨</label>
|
||||
<select
|
||||
value={formData.risk_level}
|
||||
onChange={(e) => setFormData({ ...formData, risk_level: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white"
|
||||
>
|
||||
<option value="LOW">LOW (보수적)</option>
|
||||
<option value="MEDIUM">MEDIUM (중립)</option>
|
||||
<option value="HIGH">HIGH (공격적)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1">기본 손절가 (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.priority}
|
||||
onChange={(e) => setFormData({ ...formData, priority: parseInt(e.target.value) })}
|
||||
value={formData.stop_loss}
|
||||
onChange={(e) => setFormData({ ...formData, stop_loss: parseFloat(e.target.value) })}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white"
|
||||
min="1"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1">기본 익절가 (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.take_profit}
|
||||
onChange={(e) => setFormData({ ...formData, take_profit: parseFloat(e.target.value) })}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1">조건 (JSON)</label>
|
||||
<textarea
|
||||
value={formData.conditions}
|
||||
onChange={(e) => setFormData({ ...formData, conditions: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white font-mono text-xs"
|
||||
rows={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1">실행 액션 (JSON)</label>
|
||||
<textarea
|
||||
value={formData.actions}
|
||||
onChange={(e) => setFormData({ ...formData, actions: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white font-mono text-xs"
|
||||
rows={6}
|
||||
required
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1">최대 매수 수량</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.max_quantity}
|
||||
onChange={(e) => setFormData({ ...formData, max_quantity: parseInt(e.target.value) })}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white"
|
||||
min="1"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">* 실제 진입 타이밍은 AI가 시장 상황에 따라 최적화합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
@@ -202,7 +208,7 @@ const TradingRules = () => {
|
||||
className="btn-primary text-sm"
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
{createMutation.isPending ? '검증 중...' : '🤖 AI 검증 후 추가'}
|
||||
{createMutation.isPending ? '처리 중...' : '자동매매 규칙 등록'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user