전략시간봉 오류 재수정
This commit is contained in:
@@ -101,22 +101,32 @@ public class LiveStrategyEvaluator {
|
|||||||
ta4jStorage.getOrCreate(market, candleType).getBarCount());
|
ta4jStorage.getOrCreate(market, candleType).getBarCount());
|
||||||
|
|
||||||
for (GcLiveStrategySettings s : settings) {
|
for (GcLiveStrategySettings s : settings) {
|
||||||
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
|
String result = evaluateSettingOnCandleClose(s, market, candleType, maturedIndex);
|
||||||
if (!"CANDLE_CLOSE".equals(s.getExecutionType())) continue;
|
if (!"NONE".equals(result)) return result;
|
||||||
if (s.getStrategyId() == null) continue;
|
}
|
||||||
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
|
return "NONE";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단일 live 설정 — 봉 마감(CANDLE_CLOSE) 평가.
|
||||||
|
*/
|
||||||
|
public String evaluateSettingOnCandleClose(GcLiveStrategySettings s, String market,
|
||||||
|
String candleType, int maturedIndex) {
|
||||||
|
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) return "NONE";
|
||||||
|
if (!"CANDLE_CLOSE".equals(s.getExecutionType())) return "NONE";
|
||||||
|
if (s.getStrategyId() == null) return "NONE";
|
||||||
|
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
||||||
|
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
|
||||||
|
|
||||||
String result = evaluate(market, candleType, s.getStrategyId(),
|
String result = evaluate(market, candleType, s.getStrategyId(),
|
||||||
s.getPositionMode(), maturedIndex,
|
s.getPositionMode(), maturedIndex,
|
||||||
s.getDeviceId(), s.getUserId());
|
s.getDeviceId(), s.getUserId());
|
||||||
if (!"NONE".equals(result)) {
|
if (!"NONE".equals(result)) {
|
||||||
log.info("[Evaluator] CANDLE_CLOSE {} {} idx={} mode={} → {}",
|
log.info("[Evaluator] CANDLE_CLOSE strategyId={} {} {} idx={} mode={} → {}",
|
||||||
market, candleType, maturedIndex, s.getPositionMode(), result);
|
s.getStrategyId(), market, candleType, maturedIndex, s.getPositionMode(), result);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return "NONE";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 방식 B — 3초 스케줄러에서 호출.
|
* 방식 B — 3초 스케줄러에서 호출.
|
||||||
@@ -141,23 +151,32 @@ public class LiveStrategyEvaluator {
|
|||||||
List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
|
List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
|
||||||
if (settings.isEmpty()) return "NONE";
|
if (settings.isEmpty()) return "NONE";
|
||||||
|
|
||||||
|
for (GcLiveStrategySettings s : settings) {
|
||||||
|
String result = evaluateSettingRealtimeTick(s, market, candleType);
|
||||||
|
if (!"NONE".equals(result)) return result;
|
||||||
|
}
|
||||||
|
return "NONE";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단일 live 설정 — 3초 스케줄러(REALTIME_TICK) 평가.
|
||||||
|
*/
|
||||||
|
public String evaluateSettingRealtimeTick(GcLiveStrategySettings s, String market,
|
||||||
|
String candleType) {
|
||||||
|
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) return "NONE";
|
||||||
|
if (!"REALTIME_TICK".equals(s.getExecutionType())) return "NONE";
|
||||||
|
if (s.getStrategyId() == null) return "NONE";
|
||||||
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
||||||
|
|
||||||
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
|
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
|
||||||
if (series.isEmpty()) return "NONE";
|
if (series.isEmpty()) return "NONE";
|
||||||
int currentIndex = series.getEndIndex();
|
int currentIndex = series.getEndIndex();
|
||||||
|
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
|
||||||
|
|
||||||
// 동일 provisional 봉에서 이미 시그널을 냈으면 중복 발화 방지
|
String dedupeKey = market + ":" + candleType + ":" + s.getStrategyId();
|
||||||
String dedupeKey = market + ":" + candleType;
|
|
||||||
Integer lastSignaledIdx = realtimeSignaledIdx.get(dedupeKey);
|
Integer lastSignaledIdx = realtimeSignaledIdx.get(dedupeKey);
|
||||||
if (lastSignaledIdx != null && lastSignaledIdx == currentIndex) return "NONE";
|
if (lastSignaledIdx != null && lastSignaledIdx == currentIndex) return "NONE";
|
||||||
|
|
||||||
for (GcLiveStrategySettings s : settings) {
|
|
||||||
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
|
|
||||||
if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
|
|
||||||
if (s.getStrategyId() == null) continue;
|
|
||||||
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
|
|
||||||
|
|
||||||
// ★ Ta4j CachedIndicator 캐시 무효화 — Rule 트리 재빌드
|
|
||||||
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
|
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
|
||||||
triggerRulesCache.remove(cacheKey);
|
triggerRulesCache.remove(cacheKey);
|
||||||
|
|
||||||
@@ -165,14 +184,12 @@ public class LiveStrategyEvaluator {
|
|||||||
s.getPositionMode(), currentIndex,
|
s.getPositionMode(), currentIndex,
|
||||||
s.getDeviceId(), s.getUserId());
|
s.getDeviceId(), s.getUserId());
|
||||||
if (!"NONE".equals(result)) {
|
if (!"NONE".equals(result)) {
|
||||||
log.info("[Evaluator] REALTIME_TICK {} {} idx={} mode={} → {}",
|
log.info("[Evaluator] REALTIME_TICK strategyId={} {} {} idx={} mode={} → {}",
|
||||||
market, candleType, currentIndex, s.getPositionMode(), result);
|
s.getStrategyId(), market, candleType, currentIndex, s.getPositionMode(), result);
|
||||||
realtimeSignaledIdx.put(dedupeKey, currentIndex);
|
realtimeSignaledIdx.put(dedupeKey, currentIndex);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return "NONE";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 봉 마감 직후 REALTIME_TICK 설정에 대해 확정봉 인덱스로 전략을 평가한다.
|
* 봉 마감 직후 REALTIME_TICK 설정에 대해 확정봉 인덱스로 전략을 평가한다.
|
||||||
@@ -192,15 +209,25 @@ public class LiveStrategyEvaluator {
|
|||||||
public String evaluateRealtimeAtClose(String market, String candleType, int maturedIndex) {
|
public String evaluateRealtimeAtClose(String market, String candleType, int maturedIndex) {
|
||||||
List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
|
List<GcLiveStrategySettings> settings = settingsRepo.findActiveByMarket(market);
|
||||||
if (settings.isEmpty()) return "NONE";
|
if (settings.isEmpty()) return "NONE";
|
||||||
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
|
||||||
|
|
||||||
for (GcLiveStrategySettings s : settings) {
|
for (GcLiveStrategySettings s : settings) {
|
||||||
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) continue;
|
String result = evaluateSettingRealtimeAtClose(s, market, candleType, maturedIndex);
|
||||||
if (!"REALTIME_TICK".equals(s.getExecutionType())) continue;
|
if (!"NONE".equals(result)) return result;
|
||||||
if (s.getStrategyId() == null) continue;
|
}
|
||||||
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
|
return "NONE";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단일 live 설정 — 봉 마감 시점 REALTIME_TICK 평가(교차 누락 방지).
|
||||||
|
*/
|
||||||
|
public String evaluateSettingRealtimeAtClose(GcLiveStrategySettings s, String market,
|
||||||
|
String candleType, int maturedIndex) {
|
||||||
|
if (!Boolean.TRUE.equals(s.getIsLiveCheck())) return "NONE";
|
||||||
|
if (!"REALTIME_TICK".equals(s.getExecutionType())) return "NONE";
|
||||||
|
if (s.getStrategyId() == null) return "NONE";
|
||||||
|
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
||||||
|
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
|
||||||
|
|
||||||
// CachedIndicator 캐시 무효화 — 확정봉의 최종 close 로 재계산
|
|
||||||
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
|
String cacheKey = market + ":" + candleType + ":" + s.getStrategyId();
|
||||||
triggerRulesCache.remove(cacheKey);
|
triggerRulesCache.remove(cacheKey);
|
||||||
|
|
||||||
@@ -208,14 +235,12 @@ public class LiveStrategyEvaluator {
|
|||||||
s.getPositionMode(), maturedIndex,
|
s.getPositionMode(), maturedIndex,
|
||||||
s.getDeviceId(), s.getUserId());
|
s.getDeviceId(), s.getUserId());
|
||||||
if (!"NONE".equals(result)) {
|
if (!"NONE".equals(result)) {
|
||||||
log.info("[Evaluator] REALTIME_TICK @candle_close {} {} idx={} mode={} → {}",
|
log.info("[Evaluator] REALTIME_TICK @candle_close strategyId={} {} {} idx={} mode={} → {}",
|
||||||
market, candleType, maturedIndex, s.getPositionMode(), result);
|
s.getStrategyId(), market, candleType, maturedIndex, s.getPositionMode(), result);
|
||||||
realtimeSignaledIdx.put(market + ":" + candleType, maturedIndex);
|
realtimeSignaledIdx.put(market + ":" + candleType + ":" + s.getStrategyId(), maturedIndex);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return "NONE";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 설정 변경 시 해당 마켓 캐시 무효화 */
|
/** 설정 변경 시 해당 마켓 캐시 무효화 */
|
||||||
public void invalidateCache(String market) {
|
public void invalidateCache(String market) {
|
||||||
|
|||||||
@@ -170,40 +170,49 @@ public class BarBuilder {
|
|||||||
*/
|
*/
|
||||||
private void evaluateStrategyOnBarClose(String market, String candleType, int maturedIndex,
|
private void evaluateStrategyOnBarClose(String market, String candleType, int maturedIndex,
|
||||||
Bar signalBar) {
|
Bar signalBar) {
|
||||||
String closeSignal = liveStrategyEvaluator.evaluateCandleClose(market, candleType, maturedIndex);
|
List<GcLiveStrategySettings> activeSettings = liveSettingsRepo.findActiveByMarket(market).stream()
|
||||||
String signalExecType = "CANDLE_CLOSE";
|
.filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck()) && s.getStrategyId() != null)
|
||||||
|
.toList();
|
||||||
|
if (activeSettings.isEmpty()) return;
|
||||||
|
|
||||||
if ("NONE".equals(closeSignal)) {
|
long candleTimeEpoch = signalBar.getEndTime().getEpochSecond()
|
||||||
closeSignal = liveStrategyEvaluator.evaluateRealtimeAtClose(market, candleType, maturedIndex);
|
- signalBar.getTimePeriod().getSeconds();
|
||||||
|
|
||||||
|
for (GcLiveStrategySettings s : activeSettings) {
|
||||||
|
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) continue;
|
||||||
|
|
||||||
|
String closeSignal = "NONE";
|
||||||
|
String signalExecType = null;
|
||||||
|
|
||||||
|
if ("CANDLE_CLOSE".equals(s.getExecutionType())) {
|
||||||
|
closeSignal = liveStrategyEvaluator.evaluateSettingOnCandleClose(
|
||||||
|
s, market, candleType, maturedIndex);
|
||||||
|
signalExecType = "CANDLE_CLOSE";
|
||||||
|
} else if ("REALTIME_TICK".equals(s.getExecutionType())) {
|
||||||
|
closeSignal = liveStrategyEvaluator.evaluateSettingRealtimeAtClose(
|
||||||
|
s, market, candleType, maturedIndex);
|
||||||
signalExecType = "REALTIME_TICK";
|
signalExecType = "REALTIME_TICK";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("BUY".equals(closeSignal) || "SELL".equals(closeSignal)) {
|
if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) continue;
|
||||||
|
|
||||||
publishStrategySignal(market, candleType, signalBar, closeSignal);
|
publishStrategySignal(market, candleType, signalBar, closeSignal);
|
||||||
}
|
|
||||||
|
|
||||||
if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final String execType = signalExecType;
|
|
||||||
GcLiveStrategySettings activeSetting = liveSettingsRepo.findActiveByMarket(market)
|
|
||||||
.stream().filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck())
|
|
||||||
&& execType.equals(s.getExecutionType())).findFirst().orElse(null);
|
|
||||||
long candleTimeEpoch = signalBar.getEndTime().getEpochSecond()
|
|
||||||
- signalBar.getTimePeriod().getSeconds();
|
|
||||||
String devId = activeSetting != null ? activeSetting.getDeviceId() : null;
|
|
||||||
Long uid = activeSetting != null ? activeSetting.getUserId() : null;
|
|
||||||
Long stratId = activeSetting != null ? activeSetting.getStrategyId() : null;
|
|
||||||
tradeSignalService.save(
|
tradeSignalService.save(
|
||||||
devId, uid, market, stratId, null,
|
s.getDeviceId(), s.getUserId(),
|
||||||
|
market, s.getStrategyId(), null,
|
||||||
closeSignal, signalBar.getClosePrice().doubleValue(),
|
closeSignal, signalBar.getClosePrice().doubleValue(),
|
||||||
candleTimeEpoch, candleType, execType);
|
candleTimeEpoch, candleType, signalExecType);
|
||||||
if (devId != null) {
|
if (s.getDeviceId() != null) {
|
||||||
orderExecutionQueue.submitSignal(devId, uid, market, stratId, closeSignal,
|
orderExecutionQueue.submitSignal(
|
||||||
|
s.getDeviceId(), s.getUserId(), market,
|
||||||
|
s.getStrategyId(), closeSignal,
|
||||||
signalBar.getClosePrice().doubleValue());
|
signalBar.getClosePrice().doubleValue());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[BarBuilder] 시그널 처리 실패 ({}/{}): {}", market, candleType, e.getMessage());
|
log.warn("[BarBuilder] 시그널 처리 실패 ({}/{} strategyId={}): {}",
|
||||||
|
market, candleType, s.getStrategyId(), e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class LiveStrategyScheduler {
|
|||||||
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
|
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
|
||||||
if (series.isEmpty()) return;
|
if (series.isEmpty()) return;
|
||||||
|
|
||||||
String signal = evaluator.evaluateRealtimeTick(market, candleType);
|
String signal = evaluator.evaluateSettingRealtimeTick(s, market, candleType);
|
||||||
if ("BUY".equals(signal) || "SELL".equals(signal)) {
|
if ("BUY".equals(signal) || "SELL".equals(signal)) {
|
||||||
// 현재 진행 중인 캔들 정보 조회
|
// 현재 진행 중인 캔들 정보 조회
|
||||||
org.ta4j.core.Bar lastBar = series.getLastBar();
|
org.ta4j.core.Bar lastBar = series.getLastBar();
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import {
|
|||||||
saveLiveStrategySettings,
|
saveLiveStrategySettings,
|
||||||
type LiveStrategySettingsDto,
|
type LiveStrategySettingsDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
import { warnStrategyTimeframeMismatch } from '../utils/strategyTimeframeSync';
|
import {
|
||||||
|
syncStrategyTimeframesFromLayoutIfNeeded,
|
||||||
|
warnStrategyTimeframeMismatch,
|
||||||
|
} from '../utils/strategyTimeframeSync';
|
||||||
import { getKoreanName } from '../utils/marketNameCache';
|
import { getKoreanName } from '../utils/marketNameCache';
|
||||||
|
|
||||||
interface Strategy {
|
interface Strategy {
|
||||||
@@ -73,6 +76,8 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
|||||||
const next: LiveStrategySettingsDto = { ...settings, ...patch, market };
|
const next: LiveStrategySettingsDto = { ...settings, ...patch, market };
|
||||||
const prev = settings;
|
const prev = settings;
|
||||||
if (next.isLiveCheck && next.strategyId != null) {
|
if (next.isLiveCheck && next.strategyId != null) {
|
||||||
|
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId);
|
||||||
|
if (!synced) return;
|
||||||
const ok = await warnStrategyTimeframeMismatch(next.strategyId);
|
const ok = await warnStrategyTimeframeMismatch(next.strategyId);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
saveLiveStrategySettings,
|
saveLiveStrategySettings,
|
||||||
type LiveStrategySettingsDto,
|
type LiveStrategySettingsDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
|
import { syncStrategyTimeframesFromLayoutIfNeeded } from '../utils/strategyTimeframeSync';
|
||||||
import { BacktestSettingsPanel } from './BacktestSettingsPanel';
|
import { BacktestSettingsPanel } from './BacktestSettingsPanel';
|
||||||
import IndicatorMainDefaultsPanel, {
|
import IndicatorMainDefaultsPanel, {
|
||||||
type IndicatorSaveUiState,
|
type IndicatorSaveUiState,
|
||||||
@@ -930,6 +931,10 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
|
|||||||
const persistLive = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
|
const persistLive = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
|
||||||
if (virtualDriven) return;
|
if (virtualDriven) return;
|
||||||
const next: LiveStrategySettingsDto = { ...liveSettings, ...patch, market: liveMarket };
|
const next: LiveStrategySettingsDto = { ...liveSettings, ...patch, market: liveMarket };
|
||||||
|
if (next.isLiveCheck && next.strategyId != null) {
|
||||||
|
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId);
|
||||||
|
if (!synced) return;
|
||||||
|
}
|
||||||
setLiveSaving(true);
|
setLiveSaving(true);
|
||||||
try {
|
try {
|
||||||
const saved = await saveLiveStrategySettings(next);
|
const saved = await saveLiveStrategySettings(next);
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ import {
|
|||||||
syncVirtualTargetsToBackend,
|
syncVirtualTargetsToBackend,
|
||||||
stopVirtualLiveOnBackend,
|
stopVirtualLiveOnBackend,
|
||||||
} from '../utils/virtualLiveStrategySync';
|
} from '../utils/virtualLiveStrategySync';
|
||||||
import { warnStrategyTimeframeMismatch } from '../utils/strategyTimeframeSync';
|
import {
|
||||||
|
syncStrategyTimeframesFromLayoutIfNeeded,
|
||||||
|
warnStrategyTimeframeMismatch,
|
||||||
|
} from '../utils/strategyTimeframeSync';
|
||||||
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
||||||
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
|
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
|
||||||
import {
|
import {
|
||||||
@@ -284,6 +287,11 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const globalStrat = strategies.find(s => s.id === session.globalStrategyId);
|
const globalStrat = strategies.find(s => s.id === session.globalStrategyId);
|
||||||
|
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(
|
||||||
|
session.globalStrategyId,
|
||||||
|
globalStrat,
|
||||||
|
);
|
||||||
|
if (!synced) return;
|
||||||
const ok = await warnStrategyTimeframeMismatch(session.globalStrategyId, globalStrat);
|
const ok = await warnStrategyTimeframeMismatch(session.globalStrategyId, globalStrat);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { loadStrategyTimeframes, pinCandleWatch, repairStrategyTimeframes } from './backendApi';
|
import { loadStrategyTimeframes, pinCandleWatch, repairStrategyTimeframes } from './backendApi';
|
||||||
import { normalizeStartCandleType } from './strategyStartNodes';
|
import { normalizeStartCandleType } from './strategyStartNodes';
|
||||||
|
import { syncStrategyTimeframesFromLayoutIfNeeded } from './strategyTimeframeSync';
|
||||||
|
|
||||||
/** 전략 DSL 평가 분봉 전체 pin — 1m 집계·상위 봉 warm-up */
|
/** 전략 DSL 평가 분봉 전체 pin — 1m 집계·상위 봉 warm-up */
|
||||||
export async function pinStrategyEvaluationTimeframes(
|
export async function pinStrategyEvaluationTimeframes(
|
||||||
market: string,
|
market: string,
|
||||||
strategyId: number,
|
strategyId: number,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
|
await syncStrategyTimeframesFromLayoutIfNeeded(strategyId);
|
||||||
try {
|
try {
|
||||||
await repairStrategyTimeframes(strategyId);
|
await repairStrategyTimeframes(strategyId);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { loadStrategyTimeframes, saveStrategy, type StrategyDto } from './backendApi';
|
import { loadStrategy, loadStrategyTimeframes, repairStrategyTimeframes, saveStrategy, type StrategyDto } from './backendApi';
|
||||||
import { loadStrategyFlowLayout } from './strategyEditorLayoutStorage';
|
import { loadStrategyFlowLayout } from './strategyEditorLayoutStorage';
|
||||||
import {
|
import {
|
||||||
alignBuySellStartCandleTypesForSave,
|
alignBuySellStartCandleTypesForSave,
|
||||||
collectDslBranches,
|
collectDslBranches,
|
||||||
collectTimeframesFromEditorState,
|
collectTimeframesFromEditorState,
|
||||||
|
decodeConditionForEditor,
|
||||||
encodeConditionForSave,
|
encodeConditionForSave,
|
||||||
|
mergeStartMetaForLoad,
|
||||||
|
normalizeStartCombineOp,
|
||||||
type EditorConditionState,
|
type EditorConditionState,
|
||||||
} from './strategyConditionSerde';
|
} from './strategyConditionSerde';
|
||||||
import { START_NODE_ID, getStartCandleTypes, normalizeStartCandleType } from './strategyStartNodes';
|
import { getStartCandleTypes, normalizeStartCandleType } from './strategyStartNodes';
|
||||||
import type { LogicNode } from './strategyTypes';
|
import type { LogicNode } from './strategyTypes';
|
||||||
import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
|
import type { StrategyFlowLayoutStore } from './strategyEditorLayoutStorage';
|
||||||
|
|
||||||
@@ -16,10 +19,12 @@ function collectFromFlowLayout(strategyId: number): string[] {
|
|||||||
if (!layout) return [];
|
if (!layout) return [];
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
for (const side of ['buy', 'sell'] as const) {
|
for (const side of ['buy', 'sell'] as const) {
|
||||||
const meta = layout[side]?.startMeta?.[START_NODE_ID];
|
const metaMap = layout[side]?.startMeta;
|
||||||
if (!meta) continue;
|
if (!metaMap) continue;
|
||||||
|
for (const meta of Object.values(metaMap)) {
|
||||||
for (const ct of getStartCandleTypes(meta)) set.add(normalizeStartCandleType(ct));
|
for (const ct of getStartCandleTypes(meta)) set.add(normalizeStartCandleType(ct));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return [...set];
|
return [...set];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +99,72 @@ export async function warnStrategyTimeframeMismatch(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildEditorStateFromStrategy(
|
||||||
|
strategy: StrategyDto,
|
||||||
|
layout: StrategyFlowLayoutStore | null,
|
||||||
|
side: 'buy' | 'sell',
|
||||||
|
): EditorConditionState {
|
||||||
|
const decoded = decodeConditionForEditor(
|
||||||
|
(side === 'buy' ? strategy.buyCondition : strategy.sellCondition) as LogicNode | null,
|
||||||
|
);
|
||||||
|
const snap = layout?.[side];
|
||||||
|
return {
|
||||||
|
root: decoded.root,
|
||||||
|
startMeta: mergeStartMetaForLoad(decoded.startMeta, snap?.startMeta),
|
||||||
|
extraStartIds: snap?.extraStartIds?.length ? snap.extraStartIds : decoded.extraStartIds,
|
||||||
|
extraRoots: snap?.extraRoots ?? decoded.extraRoots,
|
||||||
|
startCombineOp: normalizeStartCombineOp(snap?.startCombineOp ?? decoded.startCombineOp),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* flow layout(localStorage)의 START 분봉이 서버 DSL과 다르면 편집기 저장 없이 자동 동기화.
|
||||||
|
* @returns false — layout 없음 등으로 동기화 불가
|
||||||
|
*/
|
||||||
|
export async function syncStrategyTimeframesFromLayoutIfNeeded(
|
||||||
|
strategyId: number,
|
||||||
|
strategy?: StrategyDto | null,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const uiTfs = collectUiEvaluationTimeframes(strategyId, strategy);
|
||||||
|
if (uiTfs.length === 0) return true;
|
||||||
|
|
||||||
|
let serverTfs: string[];
|
||||||
|
try {
|
||||||
|
serverTfs = await loadStrategyTimeframes(strategyId);
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const uiSet = new Set(uiTfs.map(tf => normalizeStartCandleType(tf)));
|
||||||
|
const serverSet = new Set(serverTfs.map(tf => normalizeStartCandleType(tf)));
|
||||||
|
const missingOnServer = uiTfs.filter(tf => !serverSet.has(normalizeStartCandleType(tf)));
|
||||||
|
if (missingOnServer.length === 0) return true;
|
||||||
|
|
||||||
|
const layout = loadStrategyFlowLayout(String(strategyId));
|
||||||
|
if (!layout) {
|
||||||
|
return warnStrategyTimeframeMismatch(strategyId, strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
const strat = strategy ?? await loadStrategy(strategyId);
|
||||||
|
if (!strat) return true;
|
||||||
|
|
||||||
|
const buyState = buildEditorStateFromStrategy(strat, layout, 'buy');
|
||||||
|
const sellState = buildEditorStateFromStrategy(strat, layout, 'sell');
|
||||||
|
|
||||||
|
await persistStrategyEvaluationTimeframes(
|
||||||
|
strategyId,
|
||||||
|
strat.name,
|
||||||
|
strat.description ?? '',
|
||||||
|
buyState,
|
||||||
|
sellState,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await repairStrategyTimeframes(strategyId);
|
||||||
|
} catch {
|
||||||
|
/* repair optional */
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** START 분봉 변경 시 전략 DSL을 서버에 즉시 반영 */
|
/** START 분봉 변경 시 전략 DSL을 서버에 즉시 반영 */
|
||||||
export async function persistStrategyEvaluationTimeframes(
|
export async function persistStrategyEvaluationTimeframes(
|
||||||
strategyId: number,
|
strategyId: number,
|
||||||
|
|||||||
Reference in New Issue
Block a user