feat(stock): GatePanel 자동 폼 + GlobalControls (TopN/ATR/RR + 3버튼)

This commit is contained in:
2026-05-12 14:19:36 +09:00
parent a11475db57
commit 1e8542f6c7
2 changed files with 75 additions and 4 deletions

View File

@@ -1,3 +1,41 @@
export default function GatePanel({ meta, value, onChange }) { export default function GatePanel({ meta, value, onChange }) {
return <section className="screener-card"><h3>{meta?.label ?? '게이트'}</h3><p style={{fontSize: 12, color:'#9ca3af'}}>TODO: 게이트 파라미터 (Task 4.5)</p></section>; if (!meta) return null;
const props = meta.param_schema?.properties || {};
return (
<section className="screener-card">
<h3>{meta.label}</h3>
<p style={{ fontSize: 11, color: '#9ca3af', marginTop: 0 }}>
통과 조건 통과한 종목만 점수 노드에 전달
</p>
{Object.entries(props).map(([key, prop]) => (
<GateField key={key} paramKey={key} prop={prop}
value={value?.[key] ?? meta.default_params?.[key]}
onChange={(v) => onChange({ ...value, [key]: v })} />
))}
</section>
);
}
function GateField({ paramKey, prop, value, onChange }) {
if (prop.type === 'integer') {
return (
<div className="param-row">
<label style={{ width: 160, fontSize: 12 }}>{paramKey}</label>
<input type="number" value={value ?? ''}
min={prop.minimum} onChange={(e) => onChange(parseInt(e.target.value, 10))}
style={{ flex: 1 }} />
</div>
);
}
if (prop.type === 'boolean') {
return (
<div className="param-row">
<label>
<input type="checkbox" checked={!!value} onChange={(e) => onChange(e.target.checked)} />
<span style={{ marginLeft: 6, fontSize: 12 }}>{paramKey}</span>
</label>
</div>
);
}
return null;
} }

View File

@@ -2,9 +2,42 @@ export default function GlobalControls({ settings, setSettings, onRun, onSave, o
return ( return (
<section className="screener-card"> <section className="screener-card">
<h3>실행 옵션</h3> <h3>실행 옵션</h3>
<button onClick={onRun} disabled={running}>{running ? '실행 중…' : '지금 실행 (미리보기)'}</button> <div className="param-row">
<button onClick={onSave} disabled={running} style={{ marginTop: 8 }}>스냅샷 저장</button> <label style={{ width: 80, fontSize: 12 }}>Top N</label>
<button onClick={onPersist} disabled={!dirty} style={{ marginTop: 8 }}>설정 저장</button> <input type="number" value={settings.top_n}
onChange={(e) => setSettings({ ...settings, top_n: parseInt(e.target.value, 10) })}
min={5} max={100} style={{ width: 80 }} />
</div>
<div className="param-row">
<label style={{ width: 80, fontSize: 12 }}>ATR window</label>
<input type="number" value={settings.atr_window}
onChange={(e) => setSettings({ ...settings, atr_window: parseInt(e.target.value, 10) })}
min={5} max={50} style={{ width: 80 }} />
</div>
<div className="param-row">
<label style={{ width: 80, fontSize: 12 }}>손절 ×ATR</label>
<input type="number" value={settings.atr_stop_mult} step={0.1}
onChange={(e) => setSettings({ ...settings, atr_stop_mult: parseFloat(e.target.value) })}
min={0.5} max={5} style={{ width: 80 }} />
</div>
<div className="param-row">
<label style={{ width: 80, fontSize: 12 }}>R:R 비율</label>
<input type="number" value={settings.rr_ratio} step={0.1}
onChange={(e) => setSettings({ ...settings, rr_ratio: parseFloat(e.target.value) })}
min={1} max={10} style={{ width: 80 }} />
</div>
<button onClick={onRun} disabled={running}
style={{ marginTop: 16, width: '100%', padding: 10, background: '#fbbf24', color: '#0b0f17', border: 'none', borderRadius: 6, fontWeight: 600 }}>
{running ? '실행 중…' : '지금 실행 (미리보기)'}
</button>
<button onClick={onSave} disabled={running}
style={{ marginTop: 8, width: '100%', padding: 8 }}>
스냅샷 저장
</button>
<button onClick={onPersist} disabled={!dirty}
style={{ marginTop: 8, width: '100%', padding: 8, opacity: dirty ? 1 : 0.5 }}>
설정 저장 (디폴트 갱신)
</button>
</section> </section>
); );
} }