c4e6aab7b2
- backend/ — FastAPI + JWT + 모든 REST 엔드포인트 - frontend/ — Next.js 14 + Tailwind + 7페이지 (대시보드/트레이드/거래소/자동매매/설정/내정보/로그인) - core_logic.py — 신호계산/알림 로직 분리 (기존 app_streamlit.py 에서 추출) - users_db.py + bcrypt 인증, exchange_keys.py + Fernet 암호화 - trades_db.py — 진입/청산 lifecycle 추적, signal_events raw 로그 - settings_db.py — 모든 운영 파라미터 DB 영속 저장 (RSI/거래량/펀딩비 임계값 포함) - docker-compose: frontend / backend / postgres + Traefik 라우팅 - assets/logo.svg — JUNGGOMOA 그라디언트 로고 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
4.4 KiB
TypeScript
85 lines
4.4 KiB
TypeScript
'use client';
|
||
import { useEffect, useState } from 'react';
|
||
import { api } from '@/lib/api';
|
||
import { Card, PageHeader, Input, Select, Button, Toggle, Banner } from '@/components/ui';
|
||
import { Bot, FlaskConical } from 'lucide-react';
|
||
|
||
export default function AutomationPage() {
|
||
const [cfg, setCfg] = useState<any>({});
|
||
const [creds, setCreds] = useState<any[]>([]);
|
||
const [msg, setMsg] = useState<string | null>(null);
|
||
|
||
async function load() {
|
||
const [a, c] = await Promise.all([api.get('/api/automation'), api.get('/api/exchange/credentials')]);
|
||
setCfg(a); setCreds(c.filter((x: any) => x.enabled));
|
||
}
|
||
useEffect(() => { load(); }, []);
|
||
|
||
async function save() {
|
||
await api.put('/api/automation', { values: cfg });
|
||
setMsg('✅ 저장 완료');
|
||
}
|
||
|
||
async function testBalance() {
|
||
try {
|
||
const r = await api.post('/api/automation/test/balance', {});
|
||
setMsg(r.ok ? `🧪 DryRun balance=${r.balance} USDT (${r.exchange})` : `❌ ${r.error}`);
|
||
} catch (e: any) { setMsg('❌ ' + e.message); }
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader title="🤖 자동매매 설정" subtitle="현재 어댑터 — DRY-RUN 더미 (실 주문 X). 인터페이스/설정만 갖춰진 상태." />
|
||
|
||
<Banner level="warning">⚠️ 실 주문은 거래소별 SDK 어댑터 추가 후 활성화. 지금은 신호 발생 시 stdout 로 시뮬레이션.</Banner>
|
||
|
||
<Card className="mt-5">
|
||
<div className="flex items-center gap-2 mb-4 text-blue-600">
|
||
<Bot size={16} /> <span className="font-bold text-slate-800 text-sm">자동매매 설정</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||
<Toggle checked={cfg.enabled === '1'} onChange={(v: boolean) => setCfg({ ...cfg, enabled: v ? '1' : '0' })} label="자동매매 ON (글로벌 킬스위치)" />
|
||
<Toggle checked={cfg.dry_run === '1'} onChange={(v: boolean) => setCfg({ ...cfg, dry_run: v ? '1' : '0' })} label="DRY-RUN (실 주문 X)" />
|
||
<Select label="허용 방향" value={cfg.allowed_directions || 'long,short'} onChange={(e: any) => setCfg({ ...cfg, allowed_directions: e.target.value })}>
|
||
<option value="long,short">long + short</option>
|
||
<option value="long">long only</option>
|
||
<option value="short">short only</option>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
||
<Select label="활성 거래소 키" value={cfg.active_credential || ''} onChange={(e: any) => setCfg({ ...cfg, active_credential: e.target.value })}>
|
||
<option value="">(미선택)</option>
|
||
{creds.map((c: any) => (
|
||
<option key={c.id} value={c.id}>#{c.id} {c.exchange.toUpperCase()} [{c.label || '-'}] {c.testnet ? '🧪' : '🟢'}</option>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||
<Input label="레버리지" type="number" min="1" max="125" value={cfg.leverage || '10'} onChange={(e: any) => setCfg({ ...cfg, leverage: e.target.value })} />
|
||
<Input label="포지션(잔고%)" type="number" step="0.1" value={cfg.position_size_pct || '1.0'} onChange={(e: any) => setCfg({ ...cfg, position_size_pct: e.target.value })} />
|
||
<Input label="동시 진입 최대" type="number" value={cfg.max_open_trades || '3'} onChange={(e: any) => setCfg({ ...cfg, max_open_trades: e.target.value })} />
|
||
<Input label="최소 신호 score" type="number" value={cfg.min_signal_score || '1'} onChange={(e: any) => setCfg({ ...cfg, min_signal_score: e.target.value })} />
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||
<Input label="Take Profit (%, 0=OFF)" type="number" step="0.1" value={cfg.tp_pct || '0.0'} onChange={(e: any) => setCfg({ ...cfg, tp_pct: e.target.value })} />
|
||
</div>
|
||
|
||
<div className="flex gap-2 pt-3 border-t border-slate-200">
|
||
<Button onClick={save}>💾 저장</Button>
|
||
<Button variant="secondary" onClick={testBalance}><FlaskConical size={14} className="inline mr-1" /> DryRun balance 테스트</Button>
|
||
</div>
|
||
{msg && <div className="mt-3 text-sm text-slate-600">{msg}</div>}
|
||
</Card>
|
||
|
||
<Card className="mt-4">
|
||
<div className="text-sm font-bold text-slate-800 mb-2">현재 설정 (raw)</div>
|
||
<pre className="text-xs bg-slate-50 p-3 rounded overflow-x-auto">{JSON.stringify(cfg, null, 2)}</pre>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|