보조지표 전략체크 수정

This commit is contained in:
Macbook
2026-06-17 21:49:15 +09:00
parent 1698dd9c8e
commit 3c269db6b7
7 changed files with 300 additions and 40 deletions
+2 -1
View File
@@ -58,7 +58,8 @@
"plugins": {
"updater": {
"endpoints": [
"https://stock.exdev.co.kr/desktop/updates/latest.json"
"https://stock.exdev.co.kr/desktop/updates/latest.json",
"https://exdev.co.kr/desktop/updates/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDg3MUJCMjIyRkIyNzdFMUUKUldRZWZpZjdJckliaDlMS3pvRHpmQ2lnSFNjaDQrT1pEZTdDZEJ5S2RhOXV5aU1QY3JObEEwaFUK",
"windows": {
+40
View File
@@ -71,6 +71,46 @@ curl -sS https://chatbot.exdev.co.kr/chatbot/app/ | head
서버 SSH·배포 스크립트의 `REMOTE_HOST=exdev.co.kr`**그대로** 둡니다 (서버 내부·스크립트용).
## 6. 서버(Mac Studio) 안에서 브라우저·데스크톱 앱 접속
원격에서는 `https://stock.exdev.co.kr` 이 정상이어도, **서버 본체 브라우저**는 공인 IP로 다시 들어오는 **NAT hairpin** 이 막혀 `stock` 서브도메인만 타임아웃될 수 있습니다.
| URL | 서버 내부 (hosts 미설정) | 조치 후 |
|-----|--------------------------|---------|
| `http://exdev.co.kr` | ✅ (`/etc/hosts` → 127.0.0.1) | — |
| `https://exdev.co.kr` | ✅ (Caddy가 직접 프록시) | — |
| `https://stock.exdev.co.kr` | ❌ 타임아웃 | hosts에 `stock` 추가 |
**1회 설정 (서버 콘솔 또는 SSH에서 sudo):**
```bash
sudo bash /path/to/goldenChart/scripts/setup-server-local-hosts.sh
```
또는 서버 바탕화면 `Setup-GoldenChart-Hosts.command` 더블클릭 → 관리자 암호 입력.
추가되는 항목:
```text
127.0.0.1 exdev.co.kr
127.0.0.1 stock.exdev.co.kr
127.0.0.1 chatbot.exdev.co.kr
```
**검증:**
```bash
curl -sS -o /dev/null -w "stock %{http_code}\n" https://stock.exdev.co.kr/
curl -sS https://stock.exdev.co.kr/desktop/updates/latest.json | head
```
**데스크톱 앱 자동 업데이트:** 기본 엔드포인트는 `stock.exdev.co.kr` 이므로 위 hosts 설정이 필요합니다.
신규 빌드부터는 `https://exdev.co.kr/desktop/updates/latest.json`**두 번째 fallback** 으로 포함합니다 (`desktop/src-tauri/tauri.conf.json`).
**주의:** `networksetup -setdnsservers Ethernet 127.0.0.1` 처럼 DNS를 127.0.0.1 로 바꾸지 마세요 (로컬 DNS 서버가 없으면 전체 DNS가 깨집니다). 복구: `networksetup -setdnsservers Ethernet Empty`
## 7. SSH / git remote (로컬 Mac)
**로컬 Mac에서 `git push` 시 `Could not resolve hostname exdev.co.kr`**
일부 네트워크(회사·VPN·ISP)에서는 apex `exdev.co.kr` DNS만 실패하고 `stock.exdev.co.kr` 은 정상입니다.
@@ -2,7 +2,8 @@
* 전략 평가 — 전략에 포함된 지표 파라미터 맵
*/
import type { StrategyDto } from './backendApi';
import { collectStrategyRegistryTypes } from './strategyToChartIndicators';
import { collectIndicatorRefs } from './strategyToChartIndicators';
import { applyStrategyRefToParams } from './strategyIndicatorSync';
export type EvalIndicatorParams = Record<string, Record<string, number | string | boolean>>;
@@ -10,10 +11,13 @@ export function buildEvalParamsFromStrategy(
strategy: StrategyDto | null | undefined,
getParams: (type: string) => Record<string, number | string | boolean>,
): EvalIndicatorParams {
const types = collectStrategyRegistryTypes(strategy);
if (!strategy) return {};
const out: EvalIndicatorParams = {};
for (const type of types) {
out[type] = { ...getParams(type) };
for (const ref of collectIndicatorRefs(strategy)) {
const base = { ...getParams(ref.registryType) };
const merged = applyStrategyRefToParams(ref, base);
out[ref.registryType] = { ...(out[ref.registryType] ?? {}), ...merged };
}
return out;
}
@@ -0,0 +1,96 @@
/**
* 전략 DSL 기간·임계값 → 차트 지표 params / hline 동기화
* (Ta4j StrategyDslToTa4jAdapter.effectivePeriod 키와 동일)
*/
import type { ConditionDSL } from './strategyTypes';
import { isThresholdSymbol } from './thresholdSymbols';
export interface StrategyIndicatorRef {
dslType: string;
registryType: string;
period?: number;
leftPeriod?: number;
rightPeriod?: number;
targetValue?: number | null;
}
/** 조건 rightField·targetValue → 차트에 추가할 숫자 임계 hline (HL_* 는 DB visual 사용) */
export function resolveConditionThresholdForChart(c: ConditionDSL): number | null {
if (c.thresholdOverride === false) return null;
if (c.targetValue != null && Number.isFinite(c.targetValue)) return c.targetValue;
if (c.compareValue != null && Number.isFinite(c.compareValue)) return c.compareValue;
const rf = c.rightField ?? '';
if (rf.startsWith('K_')) {
const n = parseFloat(rf.slice(2));
return Number.isFinite(n) ? n : null;
}
if (isThresholdSymbol(rf)) return null;
return null;
}
const REGISTRY_PRIMARY_PERIOD_KEY: Record<string, string> = {
RSI: 'length',
CCI: 'length',
WilliamsPercentRange: 'length',
Stochastic: 'kLength',
MACD: 'fastLength',
ADX: 'adxSmoothing',
DMI: 'diLength',
TRIX: 'length',
BollingerBands: 'length',
DonchianChannels: 'length',
Envelope: 'length',
Disparity: 'length',
Psychological: 'length',
NewPsychological: 'length',
InvestPsychological: 'length',
VR: 'length',
VolumeOscillator: 'shortLength',
MFI: 'length',
ATR: 'length',
ChandeMO: 'length',
DPO: 'length',
RVI: 'length',
BBPercentB: 'length',
BBBandWidth: 'length',
StochRSI: 'lengthRSI',
};
function setIfPositive(params: Record<string, number | string | boolean>, key: string, value?: number) {
if (value != null && value > 0) params[key] = value;
}
/** 전략 ref 기간을 지표 params에 덮어씀 (DB 기본값보다 우선) */
export function applyStrategyRefToParams(
ref: StrategyIndicatorRef,
params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
const next = { ...params };
const primaryKey = REGISTRY_PRIMARY_PERIOD_KEY[ref.registryType];
if (primaryKey && ref.period != null && ref.period > 0) {
next[primaryKey] = ref.period;
}
switch (ref.registryType) {
case 'SMA':
case 'EMA':
if (ref.period != null && ref.period > 0) next.length = ref.period;
setIfPositive(next, 'fastLength', ref.leftPeriod);
setIfPositive(next, 'slowLength', ref.rightPeriod);
break;
case 'MACross':
setIfPositive(next, 'fastLength', ref.leftPeriod ?? ref.period);
setIfPositive(next, 'slowLength', ref.rightPeriod);
break;
case 'Stochastic':
setIfPositive(next, 'dSmoothing', ref.rightPeriod);
break;
case 'StochRSI':
setIfPositive(next, 'lengthStoch', ref.rightPeriod);
break;
default:
break;
}
return next;
}
+10 -10
View File
@@ -1,7 +1,7 @@
import type { OHLCVBar } from '../types';
import type { StrategyDto } from './backendApi';
import { extractVirtualConditions } from './virtualStrategyConditions';
import { formatIndicatorDisplayLabel } from './indicatorRegistry';
import { collectIndicatorRefs } from './strategyToChartIndicators';
const DSL_TO_REGISTRY: Record<string, string> = {
RSI: 'RSI',
@@ -132,23 +132,23 @@ function refLinesFor(registryType: string): number[] | undefined {
export function collectStrategyOscillatorPanes(strategy: StrategyDto | undefined): OscillatorPaneSpec[] {
if (!strategy) return [];
const rows = extractVirtualConditions(strategy);
const seen = new Set<string>();
const out: OscillatorPaneSpec[] = [];
for (const row of rows) {
if (OVERLAY_DSL.has(row.indicatorType)) continue;
const registryType = DSL_TO_REGISTRY[row.indicatorType] ?? row.indicatorType;
for (const ref of collectIndicatorRefs(strategy)) {
if (OVERLAY_DSL.has(ref.dslType)) continue;
const registryType = DSL_TO_REGISTRY[ref.dslType] ?? ref.registryType;
if (OVERLAY_DSL.has(registryType) || registryType === 'SMA' || registryType === 'EMA'
|| registryType === 'BollingerBands' || registryType === 'IchimokuCloud') {
continue;
}
if (seen.has(registryType)) continue;
seen.add(registryType);
const key = `${registryType}:${ref.period ?? ''}`;
if (seen.has(key)) continue;
seen.add(key);
out.push({
key: registryType,
key,
registryType,
label: formatIndicatorDisplayLabel(row.indicatorType),
period: undefined,
label: formatIndicatorDisplayLabel(ref.dslType),
period: ref.period,
});
}
return out;
+33 -25
View File
@@ -18,6 +18,11 @@ import { normalizeSmaConfig } from './smaConfig';
import { normalizeIchimokuConfig } from './ichimokuConfig';
import type { EditorConditionState } from './strategyConditionSerde';
import { collectUiEvaluationTimeframes } from './strategyTimeframeSync';
import {
applyStrategyRefToParams,
resolveConditionThresholdForChart,
type StrategyIndicatorRef,
} from './strategyIndicatorSync';
const DSL_TO_REGISTRY: Record<string, string> = {
RSI: 'RSI',
@@ -47,14 +52,7 @@ const DSL_TO_REGISTRY: Record<string, string> = {
MFI: 'MFI',
};
interface IndicatorRef {
dslType: string;
registryType: string;
period?: number;
leftPeriod?: number;
rightPeriod?: number;
targetValue?: number | null;
}
interface IndicatorRef extends StrategyIndicatorRef {}
type ParamRecord = Record<string, number | string | boolean>;
type GetParams = (type: string, defaults?: ParamRecord) => ParamRecord;
@@ -141,7 +139,7 @@ function walkIndicatorRefs(
const c = node.condition;
const dslType = c.indicatorType;
const registryType = DSL_TO_REGISTRY[dslType] ?? dslType;
const key = `${registryType}:${c.period ?? ''}:${c.leftPeriod ?? ''}:${c.rightPeriod ?? ''}:${c.targetValue ?? ''}`;
const key = `${registryType}:${c.period ?? ''}:${c.leftPeriod ?? ''}:${c.rightPeriod ?? ''}:${resolveConditionThresholdForChart(c) ?? ''}`;
if (seen.has(key)) return;
seen.add(key);
out.push({
@@ -150,7 +148,7 @@ function walkIndicatorRefs(
period: c.period,
leftPeriod: c.leftPeriod,
rightPeriod: c.rightPeriod,
targetValue: c.targetValue ?? c.compareValue ?? null,
targetValue: resolveConditionThresholdForChart(c),
});
return;
}
@@ -158,7 +156,7 @@ function walkIndicatorRefs(
node.children?.forEach(ch => walkIndicatorRefs(ch, out, seen));
}
function collectIndicatorRefs(strategy: StrategyDto, side?: 'BUY' | 'SELL'): IndicatorRef[] {
export function collectIndicatorRefs(strategy: StrategyDto, side?: 'BUY' | 'SELL'): IndicatorRef[] {
const out: IndicatorRef[] = [];
const seen = new Set<string>();
if (!side || side === 'BUY') {
@@ -215,6 +213,11 @@ function buildOneIndicator(
);
if (!cfg) return null;
cfg = {
...cfg,
params: applyStrategyRefToParams(ref, cfg.params),
};
if (ref.targetValue != null) {
cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };
}
@@ -237,25 +240,17 @@ export function buildStrategyChartIndicators(
if (!strategy) return [];
const refs = collectIndicatorRefs(strategy, side);
const byType = new Map<string, IndicatorRef>();
const byKey = new Map<string, IndicatorRef>();
for (const ref of refs) {
const existing = byType.get(ref.registryType);
if (!existing) {
byType.set(ref.registryType, ref);
continue;
const key = stableStrategyIndicatorId(ref);
if (!byKey.has(key)) {
byKey.set(key, ref);
}
byType.set(ref.registryType, {
...existing,
period: existing.period ?? ref.period,
leftPeriod: existing.leftPeriod ?? ref.leftPeriod,
rightPeriod: existing.rightPeriod ?? ref.rightPeriod,
targetValue: existing.targetValue ?? ref.targetValue,
});
}
const result: IndicatorConfig[] = [];
for (const ref of byType.values()) {
for (const ref of byKey.values()) {
const cfg = buildOneIndicator(ref, getParams, getVisual);
if (cfg) result.push(cfg);
}
@@ -384,14 +379,27 @@ export function buildStrategyEvaluationChartIndicators(
if (!strategy) return [];
const strategyTypes = new Set(collectStrategyRegistryTypes(strategy));
const refById = new Map(
collectIndicatorRefs(strategy).map(ref => [stableStrategyIndicatorId(ref), ref]),
);
let inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisual);
inds = ensurePaperChartOverlays(inds, getParams, getVisual);
return inds.map(ind => {
let cfg = applyDbVisualSettingsForStrategyChart(ind, getVisual);
const ref = refById.get(ind.id);
if (ref) {
cfg = {
...cfg,
params: applyStrategyRefToParams(ref, cfg.params),
};
if (ref.targetValue != null) {
cfg = { ...cfg, hlines: applyTargetHlines(cfg.hlines ?? [], ref.targetValue) };
}
}
if (strategyTypes.has(cfg.type)) {
cfg = { ...cfg, hidden: false };
}
return cfg;
return enrichIndicatorConfig(cfg);
});
}
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# exdev 서버(Mac Studio) — NAT hairpin 우회용 /etc/hosts 설정
# stock.exdev.co.kr · chatbot.exdev.co.kr → 127.0.0.1 (Caddy 443 / nginx 80)
#
# 사용 (서버 SSH):
# bash scripts/setup-server-local-hosts.sh
# sudo bash scripts/setup-server-local-hosts.sh # 비대화형
#
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${CYAN}[setup-local-hosts]${NC} $*"; }
ok() { echo -e "${GREEN}${NC} $*"; }
fail() { echo -e "${RED}${NC} $*"; exit 1; }
HOSTS_FILE="/etc/hosts"
MARKER="# GoldenChart local (NAT hairpin bypass)"
ENTRIES=(
"127.0.0.1 exdev.co.kr"
"127.0.0.1 stock.exdev.co.kr"
"127.0.0.1 chatbot.exdev.co.kr"
)
apply_hosts() {
local tmp
tmp="$(mktemp)"
cp "$HOSTS_FILE" "$tmp"
# 기존 exdev 관련 항목·마커 제거
grep -vE 'exdev\.co\.kr|GoldenChart local \(NAT hairpin bypass\)' "$tmp" > "${tmp}.clean" || true
mv "${tmp}.clean" "$tmp"
{
cat "$tmp"
echo ""
echo "$MARKER"
for line in "${ENTRIES[@]}"; do
echo "$line"
done
} > "${tmp}.new"
cp "$HOSTS_FILE" "${HOSTS_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
mv "${tmp}.new" "$HOSTS_FILE"
rm -f "$tmp"
}
verify() {
local ok_count=0
for host in exdev.co.kr stock.exdev.co.kr; do
if ping -c1 -t1 "$host" 2>/dev/null | grep -q '127.0.0.1'; then
ok "ping $host → 127.0.0.1"
ok_count=$((ok_count + 1))
elif dscacheutil -q host -a name "$host" 2>/dev/null | grep -q '127.0.0.1'; then
ok "DNS $host → 127.0.0.1"
ok_count=$((ok_count + 1))
else
fail "DNS 확인 실패: $host (127.0.0.1 이 아님)"
fi
done
curl -sf --connect-timeout 8 -o /dev/null -w "" "https://stock.exdev.co.kr/" \
|| fail "https://stock.exdev.co.kr 접속 실패"
ok "https://stock.exdev.co.kr → 200"
curl -sf --connect-timeout 8 -o /dev/null "https://stock.exdev.co.kr/desktop/updates/latest.json" \
|| fail "desktop latest.json HTTPS 실패"
ok "desktop updater latest.json HTTPS OK"
}
main() {
log "서버 로컬 hosts 설정 (NAT hairpin 우회)"
if [[ "${1:-}" == "--as-root" ]]; then
apply_hosts
dscacheutil -flushcache 2>/dev/null || true
killall -HUP mDNSResponder 2>/dev/null || true
verify
ok "설정 완료"
return
fi
if [[ "$(id -u)" -eq 0 ]]; then
apply_hosts
dscacheutil -flushcache 2>/dev/null || true
killall -HUP mDNSResponder 2>/dev/null || true
verify
ok "설정 완료"
return
fi
if [[ -n "${SUDO_ASKPASS:-}" ]] && [[ -x "${SUDO_ASKPASS}" ]]; then
log "SUDO_ASKPASS 사용"
sudo -A bash "$0" --as-root
return
fi
if [[ -t 0 ]]; then
log "관리자 암호 필요 (sudo)"
sudo bash "$0" --as-root
return
fi
fail "sudo 권한 필요. 서버 콘솔에서: sudo bash $0"
}
main "$@"