차트 설정 수정
This commit is contained in:
@@ -63,6 +63,7 @@ import type { PlotDef, HLineStyle } from './indicatorRegistry';
|
||||
import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils';
|
||||
import {
|
||||
applyPaneSeparatorToChartOptions,
|
||||
cloneChartPaneSeparatorOptions,
|
||||
resolveChartPaneSeparatorOptions,
|
||||
type ChartPaneSeparatorOptions,
|
||||
} from '../types/chartPaneSeparator';
|
||||
@@ -311,7 +312,8 @@ export class ChartManager {
|
||||
},
|
||||
rightPriceScale: { borderColor: t.borderColor },
|
||||
// pressedMouseMove: TradingChart 에서 커스텀 패닝 처리 (드로잉·클릭과 충돌 방지)
|
||||
handleScroll: { mouseWheel: true, pressedMouseMove: false, horzTouchDrag: true, vertTouchDrag: false },
|
||||
// horzTouchDrag: 보조 pane 터치 드래그가 메인 timeScale 과 어긋나는 현상 방지 — TradingChart 커스텀 패닝만 사용
|
||||
handleScroll: { mouseWheel: true, pressedMouseMove: false, horzTouchDrag: false, vertTouchDrag: false },
|
||||
handleScale: { axisPressedMouseMove: true, axisDoubleClickReset: true, mouseWheel: true, pinch: true },
|
||||
});
|
||||
|
||||
@@ -595,8 +597,13 @@ export class ChartManager {
|
||||
}
|
||||
|
||||
setPaneSeparatorOptions(opts: ChartPaneSeparatorOptions): void {
|
||||
this._paneSeparatorOptions = opts;
|
||||
this.chart.applyOptions(applyPaneSeparatorToChartOptions(opts));
|
||||
this._paneSeparatorOptions = cloneChartPaneSeparatorOptions(opts);
|
||||
this.chart.applyOptions(applyPaneSeparatorToChartOptions(this._paneSeparatorOptions));
|
||||
this._notifyPaneLayout();
|
||||
}
|
||||
|
||||
/** pane 구분선 오버레이 강제 재그리기 (설정 변경·탭 복귀) */
|
||||
refreshPaneSeparatorOverlay(): void {
|
||||
this._notifyPaneLayout();
|
||||
}
|
||||
|
||||
@@ -1210,9 +1217,10 @@ export class ChartManager {
|
||||
): void {
|
||||
this.liveStrategyMarkers = markers.map(m => {
|
||||
const buy = m.signal === 'BUY';
|
||||
const label = buy ? '매수' : '매도';
|
||||
const text = showPrice
|
||||
? `실시간 ${buy ? '매수' : '매도'} : ${m.price.toLocaleString()}`
|
||||
: `실시간 ${buy ? '매수' : '매도'}`;
|
||||
? `${label} : ${m.price.toLocaleString()}`
|
||||
: label;
|
||||
return {
|
||||
time: m.time,
|
||||
position: buy ? ('belowBar' as const) : ('aboveBar' as const),
|
||||
|
||||
@@ -90,6 +90,18 @@ async function requestOrThrow<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
/** 서버에 없는 전략 ID — 삭제·만료 후 반복 404 방지 (태블릿·Safari 네트워크 로그 완화) */
|
||||
const strategyMissingIds = new Set<number>();
|
||||
|
||||
export function isStrategyMissingOnServer(strategyId: number): boolean {
|
||||
return strategyId > 0 && strategyMissingIds.has(strategyId);
|
||||
}
|
||||
|
||||
function markStrategyMissingFromPath(pathOnly: string): void {
|
||||
const m = pathOnly.match(/^\/strategies\/(\d+)(?:\/|$)/);
|
||||
if (m) strategyMissingIds.add(Number(m[1]));
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T | null> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
@@ -100,8 +112,16 @@ async function request<T>(path: string, init?: RequestInit): Promise<T | null> {
|
||||
if (!res.ok) {
|
||||
const method = init?.method ?? 'GET';
|
||||
const guest403 = res.status === 403 && !hasRegisteredUser();
|
||||
const missing404 = res.status === 404 && method === 'GET';
|
||||
if (!guest403 && !missing404) {
|
||||
const pathOnly = path.split('?')[0];
|
||||
if (res.status === 404 && /^\/strategies\/\d+/.test(pathOnly)) {
|
||||
markStrategyMissingFromPath(pathOnly);
|
||||
}
|
||||
const quiet404 = res.status === 404 && (
|
||||
method === 'GET'
|
||||
|| pathOnly.includes('/repair-timeframes')
|
||||
|| /^\/strategies\/\d+/.test(pathOnly)
|
||||
);
|
||||
if (!guest403 && !quiet404) {
|
||||
console.error(`[backendApi] ${method} ${path} → ${res.status}`);
|
||||
}
|
||||
return null;
|
||||
@@ -1061,7 +1081,7 @@ export async function loadStrategies(): Promise<StrategyDto[]> {
|
||||
|
||||
/** 단일 전략 로드 */
|
||||
export async function loadStrategy(id: number): Promise<StrategyDto | null> {
|
||||
if (!hasRegisteredUser()) return null;
|
||||
if (!hasRegisteredUser() || id <= 0 || strategyMissingIds.has(id)) return null;
|
||||
return request<StrategyDto>(`/strategies/${id}`);
|
||||
}
|
||||
|
||||
@@ -1401,10 +1421,30 @@ export async function loadStrategyTimeframes(strategyId: number): Promise<string
|
||||
return list?.length ? list : ['1m'];
|
||||
}
|
||||
|
||||
/** DB DSL TIMEFRAME 래핑 보정 (3분봉 전략이 1m으로 평가되는 레거시 수정) */
|
||||
export async function repairStrategyTimeframes(strategyId: number): Promise<void> {
|
||||
if (!hasRegisteredUser()) return;
|
||||
await request(`/strategies/${strategyId}/repair-timeframes`, { method: 'POST' });
|
||||
const repairSkipIds = new Set<number>();
|
||||
const repairInflight = new Set<number>();
|
||||
|
||||
/** DB DSL TIMEFRAME 래핑 보정 — 전략 없으면 호출 안 함(404 방지) */
|
||||
export async function repairStrategyTimeframes(strategyId: number): Promise<boolean> {
|
||||
if (!hasRegisteredUser() || strategyId <= 0 || strategyMissingIds.has(strategyId)) return false;
|
||||
if (repairSkipIds.has(strategyId) || repairInflight.has(strategyId)) return false;
|
||||
|
||||
repairInflight.add(strategyId);
|
||||
try {
|
||||
const exists = await loadStrategy(strategyId);
|
||||
if (!exists) {
|
||||
repairSkipIds.add(strategyId);
|
||||
return false;
|
||||
}
|
||||
const ok = await request<StrategyDto>(
|
||||
`/strategies/${strategyId}/repair-timeframes`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
if (!ok) repairSkipIds.add(strategyId);
|
||||
return ok != null;
|
||||
} finally {
|
||||
repairInflight.delete(strategyId);
|
||||
}
|
||||
}
|
||||
|
||||
/** 차트 실시간 파이프라인 pin (STOMP warm-up) */
|
||||
|
||||
@@ -35,6 +35,8 @@ export function formatPlotColor(hex6: string, alphaPercent: number): string {
|
||||
}
|
||||
|
||||
export function plotColorCss(value: string): string {
|
||||
const v = value.trim();
|
||||
if (/^rgba?\(/i.test(v) || /^hsla?\(/i.test(v)) return v;
|
||||
const { hex6, alpha } = parsePlotColor(value);
|
||||
if (alpha >= 100) return hex6;
|
||||
const r = parseInt(hex6.slice(1, 3), 16);
|
||||
|
||||
@@ -7,12 +7,9 @@ export async function pinStrategyEvaluationTimeframes(
|
||||
market: string,
|
||||
strategyId: number,
|
||||
): Promise<string[]> {
|
||||
await syncStrategyTimeframesFromLayoutIfNeeded(strategyId);
|
||||
try {
|
||||
await repairStrategyTimeframes(strategyId);
|
||||
} catch {
|
||||
/* 서버 미배포·권한 등 — timeframes 조회만 진행 */
|
||||
}
|
||||
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(strategyId);
|
||||
if (!synced) return ['1m'];
|
||||
await repairStrategyTimeframes(strategyId);
|
||||
const raw = await loadStrategyTimeframes(strategyId);
|
||||
const timeframes = [...new Set(raw.map(tf => normalizeStartCandleType(tf)))];
|
||||
if (timeframes.length === 0) timeframes.push('1m');
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { loadStrategy, loadStrategyTimeframes, repairStrategyTimeframes, saveStrategy, type StrategyDto } from './backendApi';
|
||||
import {
|
||||
isStrategyMissingOnServer,
|
||||
loadStrategy,
|
||||
loadStrategyTimeframes,
|
||||
repairStrategyTimeframes,
|
||||
saveStrategy,
|
||||
type StrategyDto,
|
||||
} from './backendApi';
|
||||
import {
|
||||
alignBuySellStartCandleTypesForSave,
|
||||
collectDslBranches,
|
||||
@@ -143,9 +150,10 @@ function buildEditorStateFromStrategy(
|
||||
*/
|
||||
export async function syncStrategyTimeframesFromLayoutIfNeeded(
|
||||
strategyId: number,
|
||||
strategy?: StrategyDto | null,
|
||||
_strategy?: StrategyDto | null,
|
||||
): Promise<boolean> {
|
||||
const strat = (await loadStrategy(strategyId)) ?? strategy ?? null;
|
||||
if (isStrategyMissingOnServer(strategyId)) return false;
|
||||
const strat = await loadStrategy(strategyId);
|
||||
if (!strat) return false;
|
||||
|
||||
const uiTfs = collectUiEvaluationTimeframes(strategyId, strat);
|
||||
@@ -162,13 +170,10 @@ export async function syncStrategyTimeframesFromLayoutIfNeeded(
|
||||
|
||||
const layout = strat.flowLayout;
|
||||
if (!layout) {
|
||||
try {
|
||||
await repairStrategyTimeframes(strategyId);
|
||||
const repaired = await loadStrategyTimeframes(strategyId);
|
||||
return evaluationTimeframesMatch(uiTfs, repaired);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const repaired = await repairStrategyTimeframes(strategyId);
|
||||
if (!repaired) return false;
|
||||
const tfs = await loadStrategyTimeframes(strategyId);
|
||||
return evaluationTimeframesMatch(uiTfs, tfs);
|
||||
}
|
||||
|
||||
const buyState = buildEditorStateFromStrategy(strat, layout, 'buy');
|
||||
|
||||
@@ -15,11 +15,11 @@ interface Listener {
|
||||
handler: RawHandler;
|
||||
}
|
||||
|
||||
const INITIAL_DELAY = 80;
|
||||
const RECONNECT_BASE = 2000;
|
||||
const RECONNECT_MAX = 30_000;
|
||||
const INITIAL_DELAY = 350;
|
||||
const RECONNECT_BASE = 3000;
|
||||
const RECONNECT_MAX = 45_000;
|
||||
const PING_INTERVAL = 20_000;
|
||||
const DISCONNECT_IDLE_MS = 5_000;
|
||||
const DISCONNECT_IDLE_MS = 200;
|
||||
|
||||
const refCounts = new Map<UpbitWsChannel, Map<string, number>>();
|
||||
const listeners = new Set<Listener>();
|
||||
@@ -33,6 +33,7 @@ let pingTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let retryCount = 0;
|
||||
let closingIntentionally = false;
|
||||
let wsFailureLogged = false;
|
||||
|
||||
function getWsUrl(): string {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
@@ -175,6 +176,8 @@ function openSocket(): void {
|
||||
if (listenerCount() === 0) return;
|
||||
teardownSocket();
|
||||
|
||||
if (!mayOpenSocket()) return;
|
||||
|
||||
try {
|
||||
const socket = new WebSocket(getWsUrl());
|
||||
ws = socket;
|
||||
@@ -182,6 +185,7 @@ function openSocket(): void {
|
||||
|
||||
socket.onopen = () => {
|
||||
retryCount = 0;
|
||||
wsFailureLogged = false;
|
||||
notifyConnection(true);
|
||||
sendSubscribe();
|
||||
clearPing();
|
||||
@@ -197,7 +201,13 @@ function openSocket(): void {
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
console.warn('[upbitWsBroker] socket error');
|
||||
if (!wsFailureLogged) {
|
||||
wsFailureLogged = true;
|
||||
console.warn(
|
||||
'[upbitWsBroker] WebSocket 연결 실패 — 시세는 REST 폴링으로 대체됩니다. '
|
||||
+ '서버 nginx에 /upbit-ws 프록시가 있는지 확인하세요.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
@@ -207,7 +217,10 @@ function openSocket(): void {
|
||||
if (!closingIntentionally && listenerCount() > 0) scheduleReconnect();
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[upbitWsBroker] connect failed', err);
|
||||
if (!wsFailureLogged) {
|
||||
wsFailureLogged = true;
|
||||
console.warn('[upbitWsBroker] connect failed', err);
|
||||
}
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
@@ -274,8 +287,12 @@ export function subscribeUpbitWs(
|
||||
return () => {
|
||||
listeners.delete(entry);
|
||||
normalized.forEach(code => bumpRef(channel, code, -1));
|
||||
if (listenerCount() === 0) scheduleDisconnect();
|
||||
else if (ws?.readyState === WebSocket.OPEN) sendSubscribe();
|
||||
if (listenerCount() === 0) {
|
||||
cancelPendingConnect();
|
||||
scheduleDisconnect();
|
||||
} else if (ws?.readyState === WebSocket.OPEN) {
|
||||
sendSubscribe();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,6 @@ export async function removeVirtualTarget(market: string): Promise<VirtualTarget
|
||||
const targets = loadVirtualTargets();
|
||||
const item = targets.find(t => t.market === market);
|
||||
if (!item) return targets;
|
||||
if (item.pinned) return targets;
|
||||
|
||||
const session = loadVirtualSession();
|
||||
const next = targets.filter(t => t.market !== market);
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface VirtualTargetItem {
|
||||
strategyId: number | null;
|
||||
koreanName?: string;
|
||||
englishName?: string;
|
||||
/** 투자대상 고정 — ON 이면 목록·추세검색에서 삭제 불가 (gc_live_strategy_settings.is_pinned) */
|
||||
/** 투자대상 고정 — 추세검색 자동삭제 등에서 보호 (수동 삭제는 가능, gc_live_strategy_settings.is_pinned) */
|
||||
pinned?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user