Files
tradeing/frontend/app/settings/page.tsx
T
chpark c4e6aab7b2 React + FastAPI 풀 마이그레이션 — Streamlit 제거
- 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>
2026-05-06 17:27:11 +09:00

153 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useState } from 'react';
import { api } from '@/lib/api';
import { Card, PageHeader, Input, Select, Button, Toggle, Banner } from '@/components/ui';
import { Send, Bell, Target, Droplet, BarChart3 } from 'lucide-react';
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT'];
const TFS = ['1m', '3m', '5m', '15m', '30m', '1h', '4h'];
const TABS = [
{ key: 'tg', label: '텔레그램', icon: Send },
{ key: 'alert', label: '알림 / 모니터링', icon: Bell },
{ key: 'signal', label: '신호 임계값', icon: Target },
{ key: 'vol', label: '거래량 / 펀딩비', icon: Droplet },
{ key: 'chart', label: '차트', icon: BarChart3 },
];
export default function SettingsPage() {
const [s, setS] = useState<any>({});
const [tab, setTab] = useState('tg');
const [msg, setMsg] = useState<string | null>(null);
useEffect(() => { api.get('/api/settings').then(setS); }, []);
function set(k: string, v: any) { setS({ ...s, [k]: v }); }
function setTfs(arr: string[]) { setS({ ...s, alert_timeframes: arr.join(',') }); }
function tfList() { return (s.alert_timeframes || '').split(',').filter(Boolean); }
function toggleTf(tf: string) {
const list = tfList();
setTfs(list.includes(tf) ? list.filter(x => x !== tf) : [...list, tf]);
}
async function save() {
await api.put('/api/settings', { values: s });
setMsg('✅ 저장 완료. 다음 폴링부터 반영');
setTimeout(() => setMsg(null), 3000);
}
return (
<div>
<PageHeader title="⚙️ 시스템 설정" subtitle="DB 영속 저장 · 저장 즉시 알림 스레드 / 차트에 반영"
right={<Button onClick={save}>💾 </Button>} />
{msg && <div className="mb-4"><Banner level="success">{msg}</Banner></div>}
<Card>
<div className="flex gap-1 border-b border-slate-200 mb-5 -mx-1">
{TABS.map(t => (
<button key={t.key} onClick={() => setTab(t.key)}
className={`flex items-center gap-1.5 px-4 py-2 text-sm font-medium border-b-2 transition ${tab === t.key ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>
<t.icon size={14} /> {t.label}
</button>
))}
</div>
{tab === 'tg' && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Input label="Bot Token" value={s.telegram_token || ''} onChange={(e: any) => set('telegram_token', e.target.value)} placeholder="예: 1234567890:ABCDEF..." />
<Input label="Chat ID" value={s.telegram_chat_id || ''} onChange={(e: any) => set('telegram_chat_id', e.target.value)} placeholder="예: -1001234567890" />
</div>
<p className="text-xs text-slate-500"> Token plain text . DB .</p>
</div>
)}
{tab === 'alert' && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Select label="모니터링 심볼" value={s.alert_symbol || 'BTCUSDT'} onChange={(e: any) => set('alert_symbol', e.target.value)}>
{SYMBOLS.map(x => <option key={x} value={x}>{x}</option>)}
</Select>
<div>
<label className="block text-xs font-medium text-slate-600 mb-1"> (multi)</label>
<div className="flex flex-wrap gap-1.5">
{TFS.map(tf => {
const on = tfList().includes(tf);
return (
<button key={tf} type="button" onClick={() => toggleTf(tf)}
className={`px-3 py-1 rounded-full text-xs font-medium transition ${on ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`}>
{tf}
</button>
);
})}
</div>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Input label="쿨다운(초)" type="number" value={s.alert_cooldown_sec || '600'} onChange={(e: any) => set('alert_cooldown_sec', e.target.value)} />
<Input label="손절(%)" type="number" step="0.05" value={parseFloat(s.stop_loss_pct || '0.0075') * 100} onChange={(e: any) => set('stop_loss_pct', (parseFloat(e.target.value) / 100).toFixed(6))} />
<Input label="폴링(초)" type="number" value={s.polling_interval_sec || '30'} onChange={(e: any) => set('polling_interval_sec', e.target.value)} />
<Input label="forming polls" type="number" min="1" max="10" value={s.forming_stable_polls || '2'} onChange={(e: any) => set('forming_stable_polls', e.target.value)} />
</div>
<div className="flex gap-6 pt-2">
<Toggle checked={s.alert_enabled === '1'} onChange={(v: boolean) => set('alert_enabled', v ? '1' : '0')} label="알림 활성화" />
<Toggle checked={s.daily_report_enabled === '1'} onChange={(v: boolean) => set('daily_report_enabled', v ? '1' : '0')} label="일일 리포트 활성화" />
</div>
</div>
)}
{tab === 'signal' && (
<div className="space-y-4">
<div>
<div className="text-xs font-semibold text-slate-700 mb-2">RSI </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Input label="일반 롱 RSI ≤" type="number" value={s.long_rsi_max || '75'} onChange={(e: any) => set('long_rsi_max', e.target.value)} />
<Input label="일반 숏 RSI ≥" type="number" value={s.short_rsi_min || '25'} onChange={(e: any) => set('short_rsi_min', e.target.value)} />
<Input label="강한 롱 RSI ≤" type="number" value={s.strong_long_rsi_max || '65'} onChange={(e: any) => set('strong_long_rsi_max', e.target.value)} />
<Input label="강한 숏 RSI ≥" type="number" value={s.strong_short_rsi_min || '35'} onChange={(e: any) => set('strong_short_rsi_min', e.target.value)} />
</div>
</div>
<div>
<div className="text-xs font-semibold text-slate-700 mb-2"> body / </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Input label="body 최소(%)" type="number" step="0.05" value={parseFloat(s.body_pct_min || '0.002') * 100} onChange={(e: any) => set('body_pct_min', (parseFloat(e.target.value) / 100).toFixed(6))} />
<Input label="추세 꺾임 body(%)" type="number" step="0.05" value={parseFloat(s.reversal_body_pct || '0.003') * 100} onChange={(e: any) => set('reversal_body_pct', (parseFloat(e.target.value) / 100).toFixed(6))} />
<Input label="추세 꺾임 vol 배수" type="number" step="0.1" value={s.reversal_vol_mult || '1.3'} onChange={(e: any) => set('reversal_vol_mult', e.target.value)} />
</div>
</div>
</div>
)}
{tab === 'vol' && (
<div className="space-y-4">
<div>
<div className="text-xs font-semibold text-slate-700 mb-2"> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Input label="Exhaustion 배수" type="number" step="0.5" value={s.vol_exhaustion_mult || '3.0'} onChange={(e: any) => set('vol_exhaustion_mult', e.target.value)} />
<Input label="vol Net 배수" type="number" step="0.1" value={s.vol_net_mult || '2.0'} onChange={(e: any) => set('vol_net_mult', e.target.value)} />
<Input label="OI 활성도(%)" type="number" step="0.05" value={parseFloat(s.oi_active_pct || '0.001') * 100} onChange={(e: any) => set('oi_active_pct', (parseFloat(e.target.value) / 100).toFixed(6))} />
</div>
</div>
<div>
<div className="text-xs font-semibold text-slate-700 mb-2"> (: %)</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Input label="롱 과열 FR (≥)" type="number" step="0.001" value={s.fr_long_overheat || '0.005'} onChange={(e: any) => set('fr_long_overheat', e.target.value)} />
<Input label="숏 경보 FR (≤)" type="number" step="0.001" value={s.fr_short_caution || '-0.005'} onChange={(e: any) => set('fr_short_caution', e.target.value)} />
<Input label="숏 주의 FR (≤)" type="number" step="0.001" value={s.fr_short_extreme || '-0.007'} onChange={(e: any) => set('fr_short_extreme', e.target.value)} />
</div>
</div>
</div>
)}
{tab === 'chart' && (
<div className="grid grid-cols-2 gap-3">
<Input label="데스크톱 캔들 수" type="number" value={s.candle_limit_desktop || '200'} onChange={(e: any) => set('candle_limit_desktop', e.target.value)} />
<Input label="모바일 캔들 수" type="number" value={s.candle_limit_mobile || '60'} onChange={(e: any) => set('candle_limit_mobile', e.target.value)} />
</div>
)}
</Card>
</div>
);
}