보조지표 전략체크 수정
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Executable
+111
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user