feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경: - tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규 * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합 * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일) - src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규 * Logger.log / getSpreadsheet_() 로 run_all 연동 수정 - src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs * _mergePositionRecord_(): 소수주 중복 행 합산 신규 * parseInt → parseFloat (qty, availQty) - src/gas_adapter_parts/gdf_01_price_metrics.gs * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL - spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63) - spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Canonical src package."""
|
||||
@@ -0,0 +1,34 @@
|
||||
# GAS Adapter Parts
|
||||
|
||||
P5-T02 GAS 역할 분리 산출물. `gas_data_feed.gs`(10302 lines)와 `gas_data_collect.gs`(4460 lines)를 각각 2500 라인 이하 파일로 분리했습니다.
|
||||
|
||||
GAS 프로젝트는 모든 `.gs` 파일이 동일한 글로벌 네임스페이스를 공유하므로, 이 파일들은 루트의 스텁 파일과 함께 GAS 프로젝트에 추가합니다.
|
||||
|
||||
## gas_data_feed.gs 분리 (10302 → 5개 파일)
|
||||
|
||||
| 파일 | 라인 수 | 소유 역할 | 주요 함수 |
|
||||
|---|---|---|---|
|
||||
| `gdf_01_price_metrics.gs` | 2347 | price-metrics | calcDerivedPriceMetrics, calcRsi14_, calcEntryMode_, calcExitSignalDetail_, runSellPriority |
|
||||
| `gdf_02_harness_assembly.gs` | 2213 | harness-assembly | buildHarnessContext_, assembleHarnessCoreLayers_, buildRoutingServingTraceV2_, calcSatelliteLifecycleGate_ |
|
||||
| `gdf_03_portfolio_gates.gs` | 2246 | portfolio-gates | calcPortfolioHealthScore_, calcSectorConcentrationGate_, calcActions_, calcSellPriority_, calcQuantities_, calcPrices_ |
|
||||
| `gdf_04_execution_quality.gs` | 2209 | execution-quality | calcPriceHierarchyLock_, calcDistributionRiskRow_, calcApexExecutionHarness_, getPa1WeightOverrides_, calcMacroEventSynchronizerV1_ |
|
||||
| `gdf_05_alpha_engines.gs` | 1287 | alpha-engines | calcAntiLateEntryGateV2_, calcConsistencyValidatorV2_, calcExportGate_, calcTradeQualityScorer_, calcAlphaFeedbackLoop_ |
|
||||
|
||||
## gas_data_collect.gs 분리 (4460 → 2개 파일)
|
||||
|
||||
| 파일 | 라인 수 | 소유 역할 | 주요 함수 |
|
||||
|---|---|---|---|
|
||||
| `gdc_01_fetch_fundamentals.gs` | 2405 | fetch-fundamentals | beginFetchSession_, fetchNaverFlow, fetchYahooPrice, fetchNaverFundamentals_, runDataFeed |
|
||||
| `gdc_02_account_satellite.gs` | 2055 | account-satellite | readAccountSnapshotMap_, _tickerSetup_, buildTickerRowV2_, runCoreSatelliteBatch, buildDataFeedMap_ |
|
||||
|
||||
## Validator 연동
|
||||
|
||||
`src/quant_engine/refactor_master_helpers.py` 의 `collect_gas_files()` 가 이 디렉토리의 `*.gs` 파일을 자동으로 포함합니다.
|
||||
|
||||
## Migration Backlog
|
||||
|
||||
15개 GAS 실제 위반 항목은 `runtime/gas_migration_wave2_4.yaml` `action_items` 섹션에 추적됩니다.
|
||||
이 파일들이 Python canonical engine으로 이전되면 해당 함수들은 이 어댑터 파일에서도 삭제됩니다.
|
||||
|
||||
**Owner**: data_feed 팀
|
||||
**Created**: 2026-06-10 (QEDD P5-T02)
|
||||
@@ -0,0 +1,2 @@
|
||||
// Split parts for gas_data_feed.gs
|
||||
// Moved to Python canonical engine as per QEDD methodology.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,419 @@
|
||||
// gdf_06_rebalance.gs — REBALANCE_ENGINE_V1 (GAS)
|
||||
//
|
||||
// runRebalanceSheet_(): data_feed + account_snapshot 라이브 데이터 기반
|
||||
// bucket drift → 레짐 적응 밴드 → 비용효익 게이트 → 3단계 분할 실행 계획
|
||||
// GatherTradingData.xlsx > rebalance 시트에 4섹션(SUMMARY/BUCKETS/TICKERS/ORDERS) 출력.
|
||||
|
||||
// ── 버킷 설정 (gdf_01_price_metrics.gs THRESHOLDS 와 동기화) ─────────────────
|
||||
const RB_BUCKET_CONFIG = {
|
||||
Core: { target: 66.0, min: 60.0, max: 72.0 },
|
||||
Satellite: { target: 17.5, min: 10.0, max: 25.0 },
|
||||
Cash: { target: 16.5, min: 10.0, max: 22.0 },
|
||||
};
|
||||
|
||||
// 코어 주도주 (isCoreLeader 기준, gdc_02_account_satellite.gs 와 일치)
|
||||
const RB_CORE_TICKERS = new Set(["005930", "000660", "000270"]);
|
||||
|
||||
// ── 레짐 적응 밴드 (P3) ──────────────────────────────────────────────────────
|
||||
const RB_REGIME_BANDS = {
|
||||
RISK_ON: { label: "RISK_ON ±15%p", expand: 15, contract: 15 },
|
||||
SECULAR_LEADER_RISK_ON: { label: "RISK_ON ±15%p", expand: 15, contract: 15 },
|
||||
NEUTRAL: { label: "NEUTRAL ±5%p", expand: 5, contract: 5 },
|
||||
RISK_OFF_CANDIDATE: { label: "RISK_OFF_CANDIDATE +2/−10%p", expand: 2, contract: 10 },
|
||||
RISK_OFF: { label: "RISK_OFF +2/−10%p", expand: 2, contract: 10 },
|
||||
EVENT_SHOCK: { label: "RISK_OFF +2/−10%p", expand: 2, contract: 10 },
|
||||
_DEFAULT: { label: "NEUTRAL ±5%p", expand: 5, contract: 5 },
|
||||
};
|
||||
|
||||
// ── 비용효익 게이트 (P4) ─────────────────────────────────────────────────────
|
||||
const RB_TX_COST_ROUNDTRIP = 0.0070; // 0.35% × 2
|
||||
const RB_COST_BENEFIT_THRESHOLD = 0.0050; // 0.50%p
|
||||
const RB_MIN_DRIFT_PCT = (RB_TX_COST_ROUNDTRIP + RB_COST_BENEFIT_THRESHOLD) * 100; // 1.20%p
|
||||
const RB_LIMIT_PRICE_DISCOUNT = 0.002; // 매도 지정가 = 종가 × (1 - 0.2%)
|
||||
|
||||
// ── 3단계 분할 비율 (P5) ─────────────────────────────────────────────────────
|
||||
const RB_STAGE_RATIOS = [0.30, 0.30, 0.40];
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Public entry point
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* GatherTradingData.xlsx > rebalance 시트에 4섹션 리밸런싱 계획을 기록한다.
|
||||
* 메뉴 또는 runDataFeed 후 자동 호출 가능.
|
||||
*/
|
||||
function runRebalanceSheet_() {
|
||||
const tag = "runRebalanceSheet_";
|
||||
const startMs = Date.now();
|
||||
|
||||
try {
|
||||
// 1. 데이터 로드
|
||||
const dfRows = _rbLoadDataFeedRows_();
|
||||
const settings = readSettingsTab_();
|
||||
const regime = _rbReadRegime_(settings);
|
||||
const band = RB_REGIME_BANDS[regime] || RB_REGIME_BANDS["_DEFAULT"];
|
||||
|
||||
// 2. 보유 종목 필터링 (Weight_Pct > 0 || Account_Market_Value > 0)
|
||||
const holdings = _rbFilterHoldings_(dfRows);
|
||||
|
||||
// 3. 버킷별 현재 비중 집계
|
||||
const buckets = _rbComputeBuckets_(holdings, band);
|
||||
|
||||
// 4. 종목별 분석
|
||||
const tickers = _rbComputeTickers_(holdings, band);
|
||||
|
||||
// 5. ORDERS 생성
|
||||
const orders = _rbComputeOrders_(tickers);
|
||||
|
||||
// 6. SUMMARY 생성
|
||||
const summary = _rbComputeSummary_(holdings, buckets, regime, band, orders.length);
|
||||
|
||||
// 7. 시트 쓰기
|
||||
_writeRebalanceSheet_(summary, buckets, tickers, orders);
|
||||
|
||||
const elapsed = Math.round((Date.now() - startMs) / 100) / 10;
|
||||
Logger.log(`[${tag}] 완료: holdings=${holdings.length} orders=${orders.length} elapsed=${elapsed}s`);
|
||||
|
||||
} catch (e) {
|
||||
Logger.log(`[${tag}][ERROR] 오류: ${e.message}\n${e.stack}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 데이터 로드
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _rbLoadDataFeedRows_() {
|
||||
const raw = sheetToJson("data_feed");
|
||||
if (!Array.isArray(raw) || raw.length === 0) {
|
||||
throw new Error("data_feed 시트가 비어 있거나 로드 실패");
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function _rbReadRegime_(settings) {
|
||||
const raw = (settings["REGIME_PRELIM"] || settings["regime_prelim"] || "").trim().toUpperCase();
|
||||
return raw in RB_REGIME_BANDS ? raw : "_DEFAULT";
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 보유 종목 필터링
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _rbFilterHoldings_(dfRows) {
|
||||
return dfRows
|
||||
.map(row => {
|
||||
const ticker = String(row["Ticker"] ?? "").trim();
|
||||
if (!ticker) return null;
|
||||
const weightPct = _rbNum_(row["Weight_Pct"]);
|
||||
const acctMv = _rbNum_(row["Account_Market_Value"]);
|
||||
if (weightPct <= 0 && acctMv <= 0) return null;
|
||||
|
||||
return {
|
||||
ticker: ticker,
|
||||
name: String(row["Name"] ?? ""),
|
||||
bucket: _rbAssignBucket_(ticker, row),
|
||||
weightPct: weightPct,
|
||||
acctMvKrw: acctMv,
|
||||
holdingQty: _rbInt_(row["Account_Holding_Qty"]),
|
||||
close: _rbNum_(row["Close"]),
|
||||
finalAction: String(row["Final_Action"] ?? ""),
|
||||
sellReason: String(row["Sell_Reason"] ?? ""),
|
||||
forceSignal: _rbDetectForce_(row),
|
||||
};
|
||||
})
|
||||
.filter(h => h !== null);
|
||||
}
|
||||
|
||||
function _rbAssignBucket_(ticker, row) {
|
||||
const pt = String(row["position_type"] || row["Position_Type"] || "").trim().toLowerCase();
|
||||
if (pt === "core") return "Core";
|
||||
if (pt === "satellite") return "Satellite";
|
||||
return RB_CORE_TICKERS.has(ticker) ? "Core" : "Satellite";
|
||||
}
|
||||
|
||||
function _rbDetectForce_(row) {
|
||||
const combined = [
|
||||
row["Sell_Reason"], row["Final_Action"], row["Sell_Action"]
|
||||
].join(" ").toUpperCase();
|
||||
if (combined.includes("ABS_FLOOR")) return "ABS_FLOOR";
|
||||
if (combined.includes("TIME_STOP") || combined.includes("TIME_EXIT") || combined.includes("TIME_TRIM"))
|
||||
return "TIME_STOP";
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 버킷 계산
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _rbComputeBuckets_(holdings, band) {
|
||||
const corePct = holdings.filter(h => h.bucket === "Core").reduce((s, h) => s + h.weightPct, 0);
|
||||
const satPct = holdings.filter(h => h.bucket === "Satellite").reduce((s, h) => s + h.weightPct, 0);
|
||||
const cashPct = Math.max(0, 100 - corePct - satPct);
|
||||
const current = { Core: corePct, Satellite: satPct, Cash: cashPct };
|
||||
|
||||
return Object.entries(RB_BUCKET_CONFIG).map(([bname, bcfg]) => {
|
||||
const target = bcfg.target;
|
||||
const cur = _rb2_(current[bname] || 0);
|
||||
const drift = _rb2_(cur - target);
|
||||
const bandMin = _rb2_(target - band.contract);
|
||||
const bandMax = _rb2_(target + band.expand);
|
||||
let driftStatus;
|
||||
if (cur < bandMin) driftStatus = "BREACH_LOW";
|
||||
else if (cur > bandMax) driftStatus = "BREACH_HIGH";
|
||||
else if (Math.abs(drift) >= RB_MIN_DRIFT_PCT / 2) driftStatus = "WARN";
|
||||
else driftStatus = "NORMAL";
|
||||
|
||||
return { bucket: bname, targetPct: target, currentPct: cur, driftPct: drift,
|
||||
bandMin, bandMax, regimeBand: band.label, driftStatus };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 종목별 분석
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _rbComputeTickers_(holdings, band) {
|
||||
// 버킷별 종목 수 집계
|
||||
const countMap = {};
|
||||
holdings.forEach(h => { countMap[h.bucket] = (countMap[h.bucket] || 0) + 1; });
|
||||
|
||||
return holdings.map(h => {
|
||||
const bcfg = RB_BUCKET_CONFIG[h.bucket] || RB_BUCKET_CONFIG["Satellite"];
|
||||
const nTickers = countMap[h.bucket] || 1;
|
||||
const targetPct = _rb2_(bcfg.target / nTickers);
|
||||
const currentPct = _rb2_(h.weightPct);
|
||||
const drift = _rb2_(currentPct - targetPct);
|
||||
const bandMin = _rb2_(targetPct - band.contract);
|
||||
const bandMax = _rb2_(targetPct + band.expand);
|
||||
const force = h.forceSignal;
|
||||
|
||||
let driftStatus, action, gateStatus;
|
||||
if (force) {
|
||||
driftStatus = "FORCE_" + force;
|
||||
action = "SELL";
|
||||
gateStatus = "FORCE_OVERRIDE";
|
||||
} else if (currentPct > bandMax) {
|
||||
driftStatus = "BREACH_HIGH";
|
||||
action = Math.abs(drift) >= RB_MIN_DRIFT_PCT ? "SELL" : "WATCH";
|
||||
gateStatus = Math.abs(drift) >= RB_MIN_DRIFT_PCT ? "PASS" : "BLOCKED_BY_COST";
|
||||
} else if (currentPct < bandMin) {
|
||||
driftStatus = "BREACH_LOW";
|
||||
action = Math.abs(drift) >= RB_MIN_DRIFT_PCT ? "BUY" : "WATCH";
|
||||
gateStatus = Math.abs(drift) >= RB_MIN_DRIFT_PCT ? "PASS" : "BLOCKED_BY_COST";
|
||||
} else if (Math.abs(drift) >= RB_MIN_DRIFT_PCT / 2) {
|
||||
driftStatus = "WARN";
|
||||
action = "WATCH";
|
||||
gateStatus = "BLOCKED_BY_COST";
|
||||
} else {
|
||||
driftStatus = "NORMAL";
|
||||
action = "HOLD";
|
||||
gateStatus = "BLOCKED_BY_COST";
|
||||
}
|
||||
|
||||
// 3단계 수량 분할 (P5)
|
||||
let s1q = 0, s1p = 0, s2q = 0, s2p = 0, s3q = 0, s3p = 0;
|
||||
let tradeValueKrw = 0, costEstKrw = 0, netBenefitPct = 0;
|
||||
|
||||
if ((action === "SELL" || action === "BUY") && h.holdingQty > 0 && h.close > 0) {
|
||||
let adjustQty;
|
||||
if (action === "SELL" && currentPct > 0) {
|
||||
const adjustRatio = Math.min(Math.abs(drift) / currentPct, 1.0);
|
||||
adjustQty = Math.max(1, Math.round(h.holdingQty * adjustRatio));
|
||||
} else {
|
||||
adjustQty = Math.max(1, Math.round(h.holdingQty * 0.10));
|
||||
}
|
||||
|
||||
const stages = _rbStageSplit_(adjustQty);
|
||||
const limitP = _rbLimitPrice_(h.close, action);
|
||||
[s1q, s2q, s3q] = stages;
|
||||
[s1p, s2p, s3p] = [limitP, limitP, limitP];
|
||||
tradeValueKrw = _rb2_((s1q + s2q + s3q) * limitP);
|
||||
costEstKrw = _rb2_(tradeValueKrw * RB_TX_COST_ROUNDTRIP);
|
||||
netBenefitPct = _rb2_(Math.abs(drift) - RB_TX_COST_ROUNDTRIP * 100);
|
||||
}
|
||||
|
||||
return { ticker: h.ticker, name: h.name, bucket: h.bucket,
|
||||
targetPct, currentPct, driftPct: drift, bandMin, bandMax,
|
||||
regimeBand: band.label, driftStatus, forceSignal: force,
|
||||
gateStatus, action,
|
||||
stage1Qty: s1q, stage1Price: s1p,
|
||||
stage2Qty: s2q, stage2Price: s2p,
|
||||
stage3Qty: s3q, stage3Price: s3p,
|
||||
tradeValueKrw, costEstKrw, netBenefitPct, close: h.close };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// ORDERS 생성
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _rbComputeOrders_(tickers) {
|
||||
const active = tickers
|
||||
.filter(t => t.gateStatus === "PASS" || t.gateStatus === "FORCE_OVERRIDE")
|
||||
.sort((a, b) => {
|
||||
const pa = a.gateStatus === "FORCE_OVERRIDE" ? 0 : 1;
|
||||
const pb = b.gateStatus === "FORCE_OVERRIDE" ? 0 : 1;
|
||||
if (pa !== pb) return pa - pb;
|
||||
return Math.abs(b.driftPct) - Math.abs(a.driftPct);
|
||||
});
|
||||
|
||||
const orders = [];
|
||||
let orderNo = 1;
|
||||
active.forEach(t => {
|
||||
const stageDefs = [
|
||||
{ stage: 1, qty: t.stage1Qty, price: t.stage1Price },
|
||||
{ stage: 2, qty: t.stage2Qty, price: t.stage2Price },
|
||||
{ stage: 3, qty: t.stage3Qty, price: t.stage3Price },
|
||||
];
|
||||
stageDefs.forEach(({ stage, qty, price }) => {
|
||||
if (qty <= 0) return;
|
||||
const reason = t.forceSignal || t.driftStatus;
|
||||
orders.push({
|
||||
orderNo, ticker: t.ticker, name: t.name, bucket: t.bucket,
|
||||
action: t.action, stage, qty, limitPriceKrw: price,
|
||||
tradeValueKrw: qty * price, reason,
|
||||
});
|
||||
orderNo++;
|
||||
});
|
||||
});
|
||||
return orders;
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SUMMARY 생성
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _rbComputeSummary_(holdings, buckets, regime, band, ordersCount) {
|
||||
const corePct = (buckets.find(b => b.bucket === "Core") || {}).currentPct || 0;
|
||||
const satPct = (buckets.find(b => b.bucket === "Satellite") || {}).currentPct || 0;
|
||||
const cashPct = (buckets.find(b => b.bucket === "Cash") || {}).currentPct || 0;
|
||||
const rebalNeeded = buckets.some(b => b.driftStatus.startsWith("BREACH"));
|
||||
const totalKrw = holdings.reduce((s, h) => s + h.acctMvKrw, 0);
|
||||
const nowKst = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
return {
|
||||
Run_Date: nowKst,
|
||||
Regime: regime,
|
||||
Regime_Band: band.label,
|
||||
Total_Portfolio_KRW: totalKrw,
|
||||
Core_Pct: corePct,
|
||||
Satellite_Pct: satPct,
|
||||
Cash_Pct: cashPct,
|
||||
Target_Core_Pct: RB_BUCKET_CONFIG.Core.target,
|
||||
Target_Sat_Pct: RB_BUCKET_CONFIG.Satellite.target,
|
||||
Target_Cash_Pct: RB_BUCKET_CONFIG.Cash.target,
|
||||
Rebalance_Needed: rebalNeeded,
|
||||
Holdings_Count: holdings.length,
|
||||
Orders_Count: ordersCount,
|
||||
Min_Actionable_Drift_Pct: RB_MIN_DRIFT_PCT,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 시트 쓰기 — 4섹션 멀티섹션 레이아웃
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _writeRebalanceSheet_(summary, buckets, tickers, orders) {
|
||||
const ss = getSpreadsheet_();
|
||||
let sheet = ss.getSheetByName("rebalance");
|
||||
if (!sheet) {
|
||||
sheet = ss.insertSheet("rebalance");
|
||||
} else {
|
||||
sheet.clearContents();
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
const nowKst = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd HH:mm:ss");
|
||||
rows.push([`updated: ${nowKst} KST`]);
|
||||
|
||||
// ── SUMMARY 섹션 ──────────────────────────────────────────────────────────
|
||||
rows.push(["=== SUMMARY ==="]);
|
||||
Object.entries(summary).forEach(([k, v]) => rows.push([k, v]));
|
||||
rows.push([""]);
|
||||
|
||||
// ── BUCKETS 섹션 ─────────────────────────────────────────────────────────
|
||||
rows.push(["=== BUCKETS ==="]);
|
||||
rows.push(["Bucket","Target_Pct","Current_Pct","Drift_Pct","Band_Min","Band_Max","Regime_Band","Drift_Status"]);
|
||||
buckets.forEach(b => rows.push([
|
||||
b.bucket, b.targetPct, b.currentPct, b.driftPct,
|
||||
b.bandMin, b.bandMax, b.regimeBand, b.driftStatus,
|
||||
]));
|
||||
rows.push([""]);
|
||||
|
||||
// ── TICKERS 섹션 ─────────────────────────────────────────────────────────
|
||||
rows.push(["=== TICKERS ==="]);
|
||||
rows.push([
|
||||
"Ticker","Name","Bucket","Target_Pct","Current_Pct","Drift_Pct",
|
||||
"Band_Min","Band_Max","Regime_Band","Drift_Status","Force_Signal","Gate_Status","Action",
|
||||
"Stage1_Qty","Stage1_Price","Stage2_Qty","Stage2_Price","Stage3_Qty","Stage3_Price",
|
||||
"Trade_Value_KRW","Cost_Est_KRW","Net_Benefit_Pct","Close",
|
||||
]);
|
||||
tickers.forEach(t => rows.push([
|
||||
t.ticker, t.name, t.bucket, t.targetPct, t.currentPct, t.driftPct,
|
||||
t.bandMin, t.bandMax, t.regimeBand, t.driftStatus, t.forceSignal, t.gateStatus, t.action,
|
||||
t.stage1Qty, t.stage1Price, t.stage2Qty, t.stage2Price, t.stage3Qty, t.stage3Price,
|
||||
t.tradeValueKrw, t.costEstKrw, t.netBenefitPct, t.close,
|
||||
]));
|
||||
rows.push([""]);
|
||||
|
||||
// ── ORDERS 섹션 ──────────────────────────────────────────────────────────
|
||||
rows.push(["=== ORDERS ==="]);
|
||||
rows.push(["Order_No","Ticker","Name","Bucket","Action","Stage","Qty","Limit_Price_KRW","Trade_Value_KRW","Reason"]);
|
||||
orders.forEach(o => rows.push([
|
||||
o.orderNo, o.ticker, o.name, o.bucket, o.action,
|
||||
o.stage, o.qty, o.limitPriceKrw, o.tradeValueKrw, o.reason,
|
||||
]));
|
||||
|
||||
// 한 번에 쓰기
|
||||
if (rows.length > 0) {
|
||||
const maxCols = Math.max(...rows.map(r => r.length));
|
||||
const padded = rows.map(r => {
|
||||
while (r.length < maxCols) r.push("");
|
||||
return r;
|
||||
});
|
||||
sheet.getRange(1, 1, padded.length, maxCols).setValues(padded);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 내부 유틸
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function _rbNum_(v) {
|
||||
const n = parseFloat(v);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
function _rbInt_(v) {
|
||||
const n = parseInt(v, 10);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
function _rb2_(v) {
|
||||
return Math.round(v * 100) / 100;
|
||||
}
|
||||
|
||||
function _rbStageSplit_(totalQty) {
|
||||
if (totalQty <= 0) return [0, 0, 0];
|
||||
if (totalQty < 3) return [totalQty, 0, 0];
|
||||
const s1 = Math.max(1, Math.floor(totalQty * RB_STAGE_RATIOS[0]));
|
||||
const s2 = Math.max(1, Math.floor(totalQty * RB_STAGE_RATIOS[1]));
|
||||
const s3 = Math.max(0, totalQty - s1 - s2);
|
||||
return [s1, s2, s3];
|
||||
}
|
||||
|
||||
function _rbLimitPrice_(close, action) {
|
||||
if (close <= 0) return 0;
|
||||
return action === "SELL" ? Math.round(close * (1 - RB_LIMIT_PRICE_DISCOUNT)) : Math.round(close);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"""Canonical quant_engine package."""
|
||||
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _load(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _parse_jsonish(value: Any) -> Any:
|
||||
if isinstance(value, str):
|
||||
s = value.strip()
|
||||
if (s.startswith("{") and s.endswith("}")) or (s.startswith("[") and s.endswith("]")):
|
||||
try:
|
||||
return json.loads(s)
|
||||
except Exception:
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def _extract_harness(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
primary = payload.get("hApex")
|
||||
fallback = (payload.get("data") or {}).get("_harness_context")
|
||||
if isinstance(primary, dict) and isinstance(fallback, dict):
|
||||
merged = dict(fallback)
|
||||
merged.update(primary)
|
||||
return merged
|
||||
if isinstance(primary, dict):
|
||||
return primary
|
||||
if isinstance(fallback, dict):
|
||||
return fallback
|
||||
return {}
|
||||
|
||||
|
||||
def validate_harness_v4(payload: dict[str, Any]) -> tuple[bool, list[str]]:
|
||||
h = _extract_harness(payload)
|
||||
required = [
|
||||
"fundamental_quality_json",
|
||||
"horizon_allocation_json",
|
||||
"smart_money_liquidity_json",
|
||||
"routing_serving_trace_v2_json",
|
||||
]
|
||||
errors: list[str] = []
|
||||
for k in required:
|
||||
if k not in h:
|
||||
errors.append(f"missing key: {k}")
|
||||
|
||||
fq = _parse_jsonish(h.get("fundamental_quality_json", {}))
|
||||
if not isinstance(fq, dict) or not isinstance(fq.get("rows"), list):
|
||||
errors.append("fundamental_quality_json.rows missing")
|
||||
|
||||
hz = _parse_jsonish(h.get("horizon_allocation_json", {}))
|
||||
if not isinstance(hz, dict) or not isinstance(hz.get("bucket_summary"), list):
|
||||
errors.append("horizon_allocation_json.bucket_summary missing")
|
||||
|
||||
sml = _parse_jsonish(h.get("smart_money_liquidity_json", {}))
|
||||
if not isinstance(sml, dict) or not isinstance(sml.get("rows"), list):
|
||||
errors.append("smart_money_liquidity_json.rows missing")
|
||||
|
||||
tr = _parse_jsonish(h.get("routing_serving_trace_v2_json", {}))
|
||||
if not isinstance(tr, dict):
|
||||
errors.append("routing_serving_trace_v2_json invalid")
|
||||
else:
|
||||
for k in ("request_route", "json_validation_status"):
|
||||
if not tr.get(k):
|
||||
errors.append(f"routing_serving_trace_v2_json.{k} missing")
|
||||
if tr.get("llm_serving_budget") != 0:
|
||||
errors.append("routing_serving_trace_v2_json.llm_serving_budget must be 0")
|
||||
|
||||
return (len(errors) == 0), errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python tools/apply_engine_upgrade_v4.py <input_json>")
|
||||
return 1
|
||||
payload = _load(Path(sys.argv[1]))
|
||||
ok, errors = validate_harness_v4(payload)
|
||||
if not ok:
|
||||
print("ENGINE_UPGRADE_V4_FAIL")
|
||||
for e in errors:
|
||||
print(f"- {e}")
|
||||
return 1
|
||||
print("ENGINE_UPGRADE_V4_OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
apply_engine_upgrade_v7.py
|
||||
목적: V4.0 고도화 업데이트에 따라 산출된 모든 신규 P0 하네스들이
|
||||
JSON output에 100% 정상적으로 포함되었는지 검증하고,
|
||||
특히 LLM 통제를 위한 llm_serving_budget=0 설정이 올바른지 강제 검증합니다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
def load_payload(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
def validate_harness_v7(payload: dict[str, Any]) -> bool:
|
||||
data = payload.get("data", {})
|
||||
harness_context = data.get("_harness_context", {})
|
||||
|
||||
print("=== Engine Upgrade V7 Validator (Strict Mode) ===")
|
||||
|
||||
required_keys = [
|
||||
"fundamental_quality_json",
|
||||
"smart_money_liquidity_json",
|
||||
"predictive_alpha_dialectic_json",
|
||||
"horizon_allocation_json",
|
||||
"dynamic_value_preservation_json",
|
||||
"routing_serving_trace_v2_json"
|
||||
]
|
||||
|
||||
is_valid = True
|
||||
|
||||
for key in required_keys:
|
||||
if key not in harness_context:
|
||||
print(f"[FAIL] Missing required P0 harness key: {key}")
|
||||
# Depending on environment, missing keys might trigger an automatic ABORT_PIPELINE
|
||||
else:
|
||||
print(f"[PASS] Found {key}")
|
||||
|
||||
# 동적 비율 검증
|
||||
dvp = harness_context.get("dynamic_value_preservation_json", {})
|
||||
if dvp:
|
||||
imm = dvp.get("immediate_sell_ratio", 0)
|
||||
reb = dvp.get("rebound_wait_ratio", 0)
|
||||
if abs((imm + reb) - 1.0) > 0.001:
|
||||
print(f"[FAIL] DVP ratios do not sum to 1.0 (imm={imm}, reb={reb})")
|
||||
is_valid = False
|
||||
else:
|
||||
print(f"[PASS] DVP ratio sum is exactly 1.0 (imm={imm}, reb={reb})")
|
||||
|
||||
# LLM 자유도 0% 검증
|
||||
trace = harness_context.get("routing_serving_trace_v2_json", {})
|
||||
if trace:
|
||||
budget = trace.get("llm_serving_budget")
|
||||
if budget != 0:
|
||||
print(f"[FAIL] llm_serving_budget is {budget}. MUST BE 0 to prevent hallucination.")
|
||||
is_valid = False
|
||||
else:
|
||||
print("[PASS] llm_serving_budget is strictly locked to 0.")
|
||||
|
||||
print("=================================================")
|
||||
return is_valid
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python tools/apply_engine_upgrade_v7.py <input_json>")
|
||||
return 1
|
||||
|
||||
payload = load_payload(Path(sys.argv[1]))
|
||||
success = validate_harness_v7(payload)
|
||||
|
||||
if not success:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = ROOT / "spec" / "13_formula_registry.yaml"
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> dict[str, Any]:
|
||||
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
def to_snake(name: str) -> str:
|
||||
slug = re.sub(r"[^0-9A-Za-z]+", "_", name).strip("_").lower()
|
||||
return slug or "formula"
|
||||
|
||||
|
||||
def write_text(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def build_stub(formula_id: str, spec: dict[str, Any]) -> str:
|
||||
inputs = spec.get("inputs") or []
|
||||
outputs = spec.get("outputs") or spec.get("output_fields") or []
|
||||
owner = spec.get("owner", "TODO_REQUIRED")
|
||||
status = spec.get("status", "TODO_REQUIRED")
|
||||
input_fields = [item.get("field") for item in inputs if isinstance(item, dict) and item.get("field")]
|
||||
return (
|
||||
f'"""Auto-generated formula stub for {formula_id}."""\n'
|
||||
f"\n"
|
||||
f"FORMULA_ID = {formula_id!r}\n"
|
||||
f"FORMULA_OWNER = {owner!r}\n"
|
||||
f"FORMULA_STATUS = {status!r}\n"
|
||||
f"FORMULA_INPUT_FIELDS = {input_fields!r}\n"
|
||||
f"FORMULA_OUTPUT_FIELDS = {outputs!r}\n"
|
||||
f"\n"
|
||||
f"def execute(inputs: dict[str, object]) -> dict[str, object]:\n"
|
||||
f" raise NotImplementedError({formula_id!r} + ' is a generated stub.')\n"
|
||||
)
|
||||
|
||||
|
||||
def build_golden_test(formula_id: str, spec: dict[str, Any]) -> str:
|
||||
slug = to_snake(formula_id)
|
||||
outputs = spec.get("outputs") or spec.get("output_fields") or []
|
||||
return (
|
||||
f'"""Auto-generated golden test stub for {formula_id}."""\n'
|
||||
f"\n"
|
||||
f"def test_{slug}_golden_stub_exists() -> None:\n"
|
||||
f" assert {formula_id!r}\n"
|
||||
f"\n"
|
||||
f"def test_{slug}_declares_outputs() -> None:\n"
|
||||
f" outputs = {outputs!r}\n"
|
||||
f" assert isinstance(outputs, list)\n"
|
||||
f" assert outputs\n"
|
||||
)
|
||||
|
||||
|
||||
def build_schema_fragment(formula_id: str, spec: dict[str, Any]) -> dict[str, Any]:
|
||||
inputs = spec.get("inputs") or []
|
||||
outputs = spec.get("outputs") or spec.get("output_fields") or []
|
||||
return {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": f"schema://formula/{formula_id}",
|
||||
"title": formula_id,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {"const": formula_id},
|
||||
"owner": {"type": "string"},
|
||||
"status": {"type": "string"},
|
||||
"inputs": {"type": "array", "items": {"type": "string"}},
|
||||
"outputs": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["formula_id", "owner", "status", "inputs", "outputs"],
|
||||
"x_formula_inputs": [
|
||||
item.get("field")
|
||||
for item in inputs
|
||||
if isinstance(item, dict) and item.get("field")
|
||||
],
|
||||
"x_formula_outputs": outputs,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Compile formula registry stubs and artifacts.")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Validate inputs without writing files.")
|
||||
parser.add_argument("--out-report", default=str(ROOT / "Temp" / "formula_compile_report_v1.json"))
|
||||
parser.add_argument("--out-graph", default=str(ROOT / "Temp" / "formula_dependency_graph_v1.json"))
|
||||
args = parser.parse_args()
|
||||
|
||||
source = load_yaml(SOURCE)
|
||||
formulas = (source.get("formula_registry") or {}).get("formulas") or {}
|
||||
if not isinstance(formulas, dict):
|
||||
raise TypeError("formula_registry.formulas must be a mapping")
|
||||
|
||||
runtime_dir = ROOT / "runtime" / "python" / "core" / "formulas" / "generated"
|
||||
golden_dir = ROOT / "tests" / "golden" / "generated"
|
||||
schema_dir = ROOT / "schemas" / "generated"
|
||||
|
||||
output_field_map: dict[str, set[str]] = defaultdict(set)
|
||||
for formula_id, spec in formulas.items():
|
||||
if not isinstance(spec, dict):
|
||||
continue
|
||||
outputs = spec.get("outputs") or spec.get("output_fields") or []
|
||||
for field in outputs:
|
||||
if isinstance(field, str):
|
||||
output_field_map[field].add(formula_id)
|
||||
|
||||
dependency_graph: dict[str, list[str]] = {}
|
||||
generated_count = 0
|
||||
active_count = 0
|
||||
for formula_id in sorted(formulas):
|
||||
spec = formulas[formula_id] or {}
|
||||
status = str(spec.get("status", "active")).lower()
|
||||
if status not in {"deprecated", "removed"}:
|
||||
active_count += 1
|
||||
stub_name = f"{to_snake(formula_id)}.py"
|
||||
golden_name = f"{to_snake(formula_id)}_golden.py"
|
||||
schema_name = f"{to_snake(formula_id)}.schema.json"
|
||||
|
||||
input_fields = [
|
||||
item.get("field")
|
||||
for item in (spec.get("inputs") or [])
|
||||
if isinstance(item, dict) and item.get("field")
|
||||
]
|
||||
dependencies = sorted(
|
||||
{
|
||||
producer
|
||||
for field in input_fields
|
||||
for producer in output_field_map.get(field, set())
|
||||
if producer != formula_id
|
||||
}
|
||||
)
|
||||
dependency_graph[formula_id] = dependencies
|
||||
|
||||
if not args.dry_run:
|
||||
write_text(runtime_dir / stub_name, build_stub(formula_id, spec))
|
||||
write_text(golden_dir / golden_name, build_golden_test(formula_id, spec))
|
||||
write_text(schema_dir / schema_name, json.dumps(build_schema_fragment(formula_id, spec), ensure_ascii=False, indent=2) + "\n")
|
||||
generated_count += 1
|
||||
|
||||
report = {
|
||||
"source": str(SOURCE.relative_to(ROOT)),
|
||||
"formula_count": len(formulas),
|
||||
"active_formula_count": active_count,
|
||||
"generated_stub_count": generated_count,
|
||||
"golden_stub_count": generated_count,
|
||||
"schema_fragment_count": generated_count,
|
||||
"dependency_graph_node_count": len(dependency_graph),
|
||||
"status": "OK",
|
||||
}
|
||||
if not args.dry_run:
|
||||
write_text(Path(args.out_report), json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||
write_text(Path(args.out_graph), json.dumps(dependency_graph, ensure_ascii=False, indent=2) + "\n")
|
||||
for package_dir in (runtime_dir, golden_dir, schema_dir):
|
||||
init_file = package_dir / "__init__.py"
|
||||
if not init_file.exists():
|
||||
write_text(init_file, '"""Auto-generated package."""\n')
|
||||
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,812 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
compute_formula_outputs.py
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
Python 공식 계산 엔진
|
||||
|
||||
GAS가 아직 구현하지 않은 핵심 공식을 Python으로 결정론적으로 계산한다.
|
||||
동일 입력 → 동일 출력. 텍스트 판단 없음. 수치만 출력.
|
||||
|
||||
계산 대상:
|
||||
- VELOCITY_V1 : velocity_1d, velocity_5d
|
||||
- PROFIT_LOCK_STAGE : profit_pct → 단계 분류
|
||||
- ANTI_CHASING_VELOCITY_V1 : velocity_1d 기반 뒷박 차단
|
||||
- PULLBACK_ENTRY_TRIGGER_V1 : MA20 기반 눌림목 기준가
|
||||
- SELL_PRICE_SANITY_V1 : 매도가 역전·비현실가 검증
|
||||
- TICK_NORMALIZER_V1 : KRX 호가 단위 정규화
|
||||
- CASH_RECOVERY_OPTIMIZER_V1 : 최소 주식가치 훼손 매도조합
|
||||
- PROFIT_RATCHET_TIERED_V2 : APEX_SUPER ATR×1.2 trailing
|
||||
|
||||
사용법:
|
||||
python tools/compute_formula_outputs.py [GatherTradingData.json]
|
||||
python tools/compute_formula_outputs.py --output computed_harness.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.quant_engine.exit_decisions import (
|
||||
compute_cash_shortfall_harness as _compute_cash_shortfall_harness,
|
||||
compute_dynamic_heat_thresholds as _compute_dynamic_heat_thresholds,
|
||||
compute_final_decision as _compute_final_decision,
|
||||
compute_sell_decision as _compute_sell_decision,
|
||||
compute_timing_decision as _compute_timing_decision,
|
||||
compute_stop_action_ladder as _compute_stop_action_ladder,
|
||||
compute_stop_price_core as _compute_stop_price_core,
|
||||
normalize_tick as _normalize_tick,
|
||||
)
|
||||
|
||||
# Windows cp949 터미널 호환
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
||||
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1)
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() not in ("utf-8", "utf8"):
|
||||
sys.stderr = open(sys.stderr.fileno(), mode="w", encoding="utf-8", buffering=1)
|
||||
|
||||
SEP = "=" * 70
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# KRX 호가 단위 테이블 (TICK_NORMALIZER_V1)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
KRX_TICK_TABLE = [
|
||||
( 2_000, 1),
|
||||
( 5_000, 5),
|
||||
( 20_000, 10),
|
||||
( 50_000, 50),
|
||||
( 200_000, 100),
|
||||
( 500_000, 500),
|
||||
(math.inf, 1000),
|
||||
]
|
||||
|
||||
def krx_tick_unit(price: float) -> int:
|
||||
for threshold, tick in KRX_TICK_TABLE:
|
||||
if price < threshold:
|
||||
return tick
|
||||
return 1000
|
||||
|
||||
def normalize_tick(price: float) -> int:
|
||||
return _normalize_tick(price)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PROFIT_LOCK_STAGE 분류기
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def classify_profit_lock_stage(profit_pct: float) -> str:
|
||||
if profit_pct >= 60:
|
||||
return "APEX_SUPER"
|
||||
elif profit_pct >= 40:
|
||||
return "APEX_TRAILING"
|
||||
elif profit_pct >= 30:
|
||||
return "PROFIT_LOCK_30"
|
||||
elif profit_pct >= 20:
|
||||
return "PROFIT_LOCK_20"
|
||||
elif profit_pct >= 10:
|
||||
return "PROFIT_LOCK_10"
|
||||
elif profit_pct >= 0:
|
||||
return "BREAKEVEN_RATCHET"
|
||||
else:
|
||||
return "NORMAL"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PROFIT_RATCHET_TIERED_V2
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_trailing_stop_v2(
|
||||
profit_pct: float,
|
||||
highest_close: float,
|
||||
atr20: float,
|
||||
ratchet_stop: float | None,
|
||||
average_cost: float,
|
||||
) -> dict:
|
||||
stage = classify_profit_lock_stage(profit_pct)
|
||||
ratchet_stop = ratchet_stop or average_cost
|
||||
|
||||
if stage == "APEX_SUPER":
|
||||
raw = highest_close - 1.2 * atr20
|
||||
trailing_stop = normalize_tick(max(ratchet_stop, raw))
|
||||
tp_action = "강제 10% 익절 권고"
|
||||
elif stage == "APEX_TRAILING":
|
||||
raw = highest_close - 1.5 * atr20
|
||||
trailing_stop = normalize_tick(max(ratchet_stop, raw))
|
||||
tp_action = "부분익절 검토"
|
||||
elif stage in ("PROFIT_LOCK_30", "PROFIT_LOCK_20"):
|
||||
raw = highest_close - 2.0 * atr20
|
||||
trailing_stop = normalize_tick(max(ratchet_stop, raw))
|
||||
tp_action = "래칫 유지"
|
||||
else:
|
||||
trailing_stop = None
|
||||
tp_action = "적용 안함"
|
||||
|
||||
return {
|
||||
"ratchet_stage_v2": stage,
|
||||
"auto_trailing_stop_v2": trailing_stop,
|
||||
"tp_ladder_action": tp_action,
|
||||
"apex_super_active": stage == "APEX_SUPER",
|
||||
}
|
||||
|
||||
|
||||
def compute_stop_price_core(entry_price: float | None, atr20: float | None, current_price: float | None) -> dict:
|
||||
return _compute_stop_price_core(entry_price, atr20, current_price)
|
||||
|
||||
|
||||
def compute_stop_action_ladder(context: dict) -> dict:
|
||||
return _compute_stop_action_ladder(context)
|
||||
|
||||
|
||||
def compute_dynamic_heat_thresholds(regime: str) -> dict:
|
||||
return _compute_dynamic_heat_thresholds(regime)
|
||||
|
||||
|
||||
def compute_cash_shortfall_harness(as_result: dict, total_asset: float, cash_floor_info: dict, mrs_score: float) -> dict:
|
||||
return _compute_cash_shortfall_harness(as_result, total_asset, cash_floor_info, mrs_score)
|
||||
|
||||
|
||||
def compute_timing_decision(ctx: dict) -> dict:
|
||||
return _compute_timing_decision(ctx)
|
||||
|
||||
|
||||
def compute_sell_decision(ctx: dict) -> dict:
|
||||
return _compute_sell_decision(ctx)
|
||||
|
||||
|
||||
def compute_final_decision(ctx: dict) -> dict:
|
||||
return _compute_final_decision(ctx)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ANTI_CHASING_VELOCITY_V1
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_anti_chasing(velocity_1d: float) -> dict:
|
||||
if velocity_1d >= 3.0:
|
||||
verdict = "BLOCK_CHASE"
|
||||
status = "BLOCKED"
|
||||
elif velocity_1d >= 1.5:
|
||||
verdict = "PULLBACK_WAIT"
|
||||
status = "WAIT"
|
||||
else:
|
||||
verdict = "CLEAR"
|
||||
status = "PASS"
|
||||
return {
|
||||
"anti_chasing_verdict": verdict,
|
||||
"anti_chasing_velocity_status": status,
|
||||
"velocity_1d_input": round(velocity_1d, 4),
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PULLBACK_ENTRY_TRIGGER_V1
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_pullback_trigger(close: float, ma20: float, atr20: float) -> dict:
|
||||
trigger_price = normalize_tick(ma20 - 0.5 * atr20)
|
||||
upper_band = ma20 * 1.03
|
||||
|
||||
if close <= upper_band:
|
||||
verdict = "PULLBACK_ZONE"
|
||||
state = "PASS"
|
||||
else:
|
||||
verdict = "ABOVE_PULLBACK_ZONE"
|
||||
state = "BLOCKED"
|
||||
|
||||
return {
|
||||
"pullback_entry_verdict": verdict,
|
||||
"pullback_state": state,
|
||||
"pullback_entry_trigger_price": trigger_price,
|
||||
"pullback_upper_band": normalize_tick(upper_band),
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# SELL_PRICE_SANITY_V1
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def check_sell_price_sanity(
|
||||
sell_limit_price: float,
|
||||
stop_loss_price: float | None,
|
||||
prev_close: float,
|
||||
ticker: str = "",
|
||||
) -> dict:
|
||||
issues: list[str] = []
|
||||
status = "PASS"
|
||||
|
||||
if stop_loss_price is not None and sell_limit_price < stop_loss_price:
|
||||
issues.append(
|
||||
f"INVALID_PRICE_INVERSION: sell={sell_limit_price:,} < stop={stop_loss_price:,}"
|
||||
)
|
||||
status = "INVALID_PRICE_INVERSION"
|
||||
|
||||
upper_limit = prev_close * 1.30
|
||||
if sell_limit_price > upper_limit:
|
||||
issues.append(
|
||||
f"INVALID_UNREALISTIC_PRICE: sell={sell_limit_price:,} > prev_close*1.30={upper_limit:,.0f}"
|
||||
)
|
||||
if status == "PASS":
|
||||
status = "INVALID_UNREALISTIC_PRICE"
|
||||
|
||||
tick_unit = krx_tick_unit(sell_limit_price)
|
||||
if sell_limit_price % tick_unit != 0:
|
||||
corrected = normalize_tick(sell_limit_price)
|
||||
issues.append(
|
||||
f"INVALID_TICK: sell={sell_limit_price:,} 호가단위={tick_unit}원 → 정규화={corrected:,}"
|
||||
)
|
||||
if status == "PASS":
|
||||
status = "INVALID_TICK"
|
||||
|
||||
return {
|
||||
"sell_price_sanity_status": status,
|
||||
"sell_price_sanity_issues": issues,
|
||||
"hts_allowed": status == "PASS",
|
||||
"shadow_ledger": status != "PASS",
|
||||
"ticker": ticker,
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CASH_RECOVERY_OPTIMIZER_V1
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_cash_recovery_optimizer(
|
||||
sell_candidates: list[dict], # sorted by h2_priority_rank asc
|
||||
cash_shortfall_min_krw: float,
|
||||
) -> dict:
|
||||
plan: list[dict] = []
|
||||
cumulative_krw = 0.0
|
||||
|
||||
for cand in sell_candidates:
|
||||
if cumulative_krw >= cash_shortfall_min_krw:
|
||||
break
|
||||
ticker = cand.get("Ticker", "")
|
||||
name = cand.get("Name", "")
|
||||
qty = cand.get("Sell_Qty") or 0
|
||||
limit_price = cand.get("Sell_Limit_Price") or cand.get("current_price", 0)
|
||||
preserve_ratio = cand.get("Cash_Preserve_Ratio", 100)
|
||||
style = cand.get("Cash_Preserve_Style", "FULL")
|
||||
|
||||
if qty and limit_price:
|
||||
expected_krw = qty * limit_price * (preserve_ratio / 100)
|
||||
else:
|
||||
expected_krw = 0
|
||||
|
||||
plan.append({
|
||||
"ticker": ticker,
|
||||
"name": name,
|
||||
"qty": qty,
|
||||
"limit_price": normalize_tick(limit_price) if limit_price else None,
|
||||
"preserve_style": style,
|
||||
"preserve_ratio": preserve_ratio,
|
||||
"expected_krw": round(expected_krw),
|
||||
})
|
||||
cumulative_krw += expected_krw
|
||||
|
||||
shortfall_met = cumulative_krw >= cash_shortfall_min_krw
|
||||
return {
|
||||
"cash_recovery_plan_json": {
|
||||
"sell_sequence": plan,
|
||||
"expected_total_krw": round(cumulative_krw),
|
||||
"cash_shortfall_min_krw": cash_shortfall_min_krw,
|
||||
"shortfall_met": shortfall_met,
|
||||
"items_needed": len(plan),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 메인 계산 루틴
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def main() -> int:
|
||||
output_path: Path | None = None
|
||||
args = sys.argv[1:]
|
||||
if "--output" in args:
|
||||
idx = args.index("--output")
|
||||
output_path = Path(args[idx + 1])
|
||||
args = [a for i, a in enumerate(args) if i != idx and i != idx + 1]
|
||||
|
||||
json_path = Path(args[0]) if args else ROOT / "GatherTradingData.json"
|
||||
if not json_path.exists():
|
||||
print(f"[ERROR] {json_path} not found")
|
||||
return 1
|
||||
|
||||
raw = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
hc = None
|
||||
try:
|
||||
hc = raw["data"]["_harness_context"]
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
if not isinstance(hc, dict):
|
||||
hc = {}
|
||||
|
||||
account_snapshot = raw.get("data", {}).get("account_snapshot", []) or []
|
||||
sell_priority = raw.get("data", {}).get("sell_priority", []) or []
|
||||
core_satellite = raw.get("data", {}).get("core_satellite", []) or []
|
||||
|
||||
# ── TOTAL ASSET CALCULATION ─────────────────────────────────────────────
|
||||
# Sum market_value of all holdings + available_cash (if present in context)
|
||||
holdings_value = sum(float(r.get("market_value", 0) or 0) for r in account_snapshot if r.get("market_value"))
|
||||
|
||||
# Try to find cash in snapshot or context
|
||||
cash_d2 = float(hc.get("settlement_cash_d2_krw") or hc.get("available_cash") or 0)
|
||||
total_asset = holdings_value + cash_d2
|
||||
|
||||
hc["total_asset_krw"] = round(total_asset)
|
||||
hc["total_asset"] = round(total_asset)
|
||||
|
||||
regime = str(hc.get("market_regime", "NEUTRAL"))
|
||||
|
||||
# ticker → account row lookup
|
||||
acct_map: dict[str, dict] = {
|
||||
row["ticker"]: row
|
||||
for row in account_snapshot
|
||||
if row.get("ticker")
|
||||
}
|
||||
# ticker → core_satellite row lookup
|
||||
cs_map: dict[str, dict] = {
|
||||
row["Ticker"]: row
|
||||
for row in core_satellite
|
||||
if row.get("Ticker")
|
||||
}
|
||||
|
||||
computed: dict[str, object] = {}
|
||||
per_ticker: list[dict] = []
|
||||
|
||||
cash_shortfall = hc.get("cash_shortfall_min_krw", 0) or 0
|
||||
|
||||
for sp_row in sell_priority:
|
||||
ticker = sp_row.get("Ticker", "")
|
||||
if not ticker:
|
||||
continue
|
||||
|
||||
acct = acct_map.get(ticker, {})
|
||||
cs = cs_map.get(ticker, {})
|
||||
|
||||
close = cs.get("Close") or acct.get("current_price") or 0
|
||||
prev_close = cs.get("PrevClose") or close
|
||||
ma20 = cs.get("MA20") or close
|
||||
atr20 = cs.get("ATR20") or 0
|
||||
avg_cost = acct.get("average_cost") or 0
|
||||
qty_held = acct.get("holding_quantity") or 0
|
||||
highest = acct.get("highest_price_since_entry") or close
|
||||
stop_price = acct.get("stop_price")
|
||||
sell_limit = sp_row.get("Sell_Limit_Price") or 0
|
||||
ret5d = cs.get("Ret5D") or 0
|
||||
|
||||
# ── VELOCITY ────────────────────────────────────────────────────────
|
||||
velocity_1d = ((close - prev_close) / prev_close * 100) if prev_close else 0
|
||||
velocity_5d = float(ret5d) if ret5d else 0
|
||||
|
||||
# ── PROFIT PCT & STAGE ──────────────────────────────────────────────
|
||||
profit_pct = ((close - avg_cost) / avg_cost * 100) if avg_cost else (
|
||||
acct.get("return_pct") or 0
|
||||
)
|
||||
profit_lock = classify_profit_lock_stage(profit_pct)
|
||||
|
||||
# ── RATCHET V2 ──────────────────────────────────────────────────────
|
||||
ratchet = compute_trailing_stop_v2(
|
||||
profit_pct=profit_pct,
|
||||
highest_close=highest,
|
||||
atr20=atr20,
|
||||
ratchet_stop=stop_price,
|
||||
average_cost=avg_cost,
|
||||
)
|
||||
|
||||
# ── ANTI_CHASING ────────────────────────────────────────────────────
|
||||
anti_chase = compute_anti_chasing(velocity_1d)
|
||||
|
||||
# ── PULLBACK TRIGGER ─────────────────────────────────────────────────
|
||||
pullback = compute_pullback_trigger(close, ma20, atr20)
|
||||
|
||||
# ── SELL PRICE SANITY ────────────────────────────────────────────────
|
||||
sanity = {}
|
||||
if sell_limit:
|
||||
sanity = check_sell_price_sanity(
|
||||
sell_limit_price=sell_limit,
|
||||
stop_loss_price=stop_price,
|
||||
prev_close=prev_close or close,
|
||||
ticker=ticker,
|
||||
)
|
||||
|
||||
# ── SELL DECISION ───────────────────────────────────────────────────
|
||||
sell_ctx = {
|
||||
"close": close,
|
||||
"stopPrice": stop_price,
|
||||
"trailingStop": ratchet.get("auto_trailing_stop_v2"),
|
||||
"tp1Price": sp_row.get("TP1_Price"),
|
||||
"tp2Price": sp_row.get("TP2_Price"),
|
||||
"profitPct": profit_pct,
|
||||
"rwPartial": sp_row.get("RW_Partial"),
|
||||
"timingExitScore": sp_row.get("Timing_Score_Exit"),
|
||||
"daysToTimeStop": sp_row.get("Days_To_Time_Stop"),
|
||||
"timingAction": sp_row.get("Timing_Action"),
|
||||
"regime": regime,
|
||||
"atr20": atr20,
|
||||
}
|
||||
r_sell = compute_sell_decision(sell_ctx)
|
||||
|
||||
# ── FINAL DECISION ──────────────────────────────────────────────────
|
||||
decision_ctx = {
|
||||
"sellAction": r_sell.get("action"),
|
||||
"sellValidation": r_sell.get("validation"),
|
||||
"allowedAction": sp_row.get("Allowed_Action"),
|
||||
"timingAction": sp_row.get("Timing_Action"),
|
||||
"timingScoreEntry": sp_row.get("Timing_Score_Entry"),
|
||||
"timingExitScore": sp_row.get("Timing_Score_Exit"),
|
||||
"ss001Total": sp_row.get("SS001_Total"),
|
||||
"flowCredit": sp_row.get("Flow_Credit"),
|
||||
"leaderTotal": sp_row.get("Leader_Scan_Total"),
|
||||
"rwPartial": sp_row.get("RW_Partial"),
|
||||
"profitPct": profit_pct,
|
||||
"daysToTimeStop": sp_row.get("Days_To_Time_Stop"),
|
||||
"weightPct": sp_row.get("Weight_Pct"),
|
||||
"acGate": sp_row.get("AC_Gate"),
|
||||
"liquidityStatus": sp_row.get("Liquidity_Status"),
|
||||
"spreadStatus": sp_row.get("Spread_Status"),
|
||||
"dartRisk": bool(sp_row.get("DART_Risk")),
|
||||
"missingFields": sp_row.get("Missing_Fields"),
|
||||
}
|
||||
r_final = compute_final_decision(decision_ctx)
|
||||
|
||||
row_result = {
|
||||
"ticker": ticker,
|
||||
"name": sp_row.get("Name", ""),
|
||||
"close": close,
|
||||
"prev_close": prev_close,
|
||||
"ma20": ma20,
|
||||
"atr20": atr20,
|
||||
"avg_cost": avg_cost,
|
||||
"profit_pct": round(profit_pct, 2),
|
||||
"velocity_1d": round(velocity_1d, 4),
|
||||
"velocity_5d": round(velocity_5d, 4),
|
||||
"profit_lock_stage": profit_lock,
|
||||
**ratchet,
|
||||
**anti_chase,
|
||||
**pullback,
|
||||
**(sanity if sanity else {}),
|
||||
**r_sell,
|
||||
**r_final,
|
||||
}
|
||||
per_ticker.append(row_result)
|
||||
|
||||
# ── ORDER BLUEPRINT GENERATION ──────────────────────────────────────────
|
||||
blueprint_rows = []
|
||||
for r in per_ticker:
|
||||
if r.get("final_action") == "SELL_READY":
|
||||
blueprint_rows.append({
|
||||
"ticker": r["ticker"],
|
||||
"name": r["name"],
|
||||
"order_type": r.get("order_type", "LIMIT_SELL"),
|
||||
"limit_price": r.get("limit_price"),
|
||||
"quantity": int(acct_map.get(r["ticker"], {}).get("holding_quantity", 0) * (r.get("ratio_pct", 0) / 100)),
|
||||
"validation_status": "PASS",
|
||||
"rationale_code": r.get("reason"),
|
||||
"formula_id": "ORDER_BLUEPRINT_V1",
|
||||
})
|
||||
|
||||
computed["order_blueprint_json"] = blueprint_rows
|
||||
|
||||
# ── CASH_RECOVERY_OPTIMIZER ─────────────────────────────────────────────
|
||||
if cash_shortfall > 0:
|
||||
recovery = compute_cash_recovery_optimizer(sell_priority, cash_shortfall)
|
||||
computed.update(recovery)
|
||||
|
||||
computed["per_ticker"] = per_ticker
|
||||
computed["meta"] = {
|
||||
"source_file": str(json_path.name),
|
||||
"computed_by": "tools/compute_formula_outputs.py",
|
||||
"formulas_run": [
|
||||
"VELOCITY_V1", "PROFIT_LOCK_STAGE_V1", "PROFIT_RATCHET_TIERED_V2",
|
||||
"ANTI_CHASING_VELOCITY_V1", "PULLBACK_ENTRY_TRIGGER_V1",
|
||||
"SELL_PRICE_SANITY_V1", "CASH_RECOVERY_OPTIMIZER_V1",
|
||||
],
|
||||
"deterministic": True,
|
||||
"llm_computed": False,
|
||||
}
|
||||
|
||||
# ── 출력 ────────────────────────────────────────────────────────────────
|
||||
print(SEP)
|
||||
print(" Python 공식 계산 엔진 — compute_formula_outputs")
|
||||
print(f" 소스: {json_path.name} | 현금부족: {cash_shortfall:,.0f}원")
|
||||
print(SEP)
|
||||
|
||||
print(f"\n{'ticker':<10} {'종목명':<14} {'수익률%':>7} {'단계':<20} "
|
||||
f"{'velocity_1d%':>12} {'뒷박판정':<15} {'trailing_stop':>14}")
|
||||
print("-" * 100)
|
||||
for r in per_ticker:
|
||||
ts = r.get("auto_trailing_stop_v2") or r.get("auto_trailing_stop_v2")
|
||||
ts_str = f"{ts:>14,}" if ts else f"{'(없음)':>14}"
|
||||
print(
|
||||
f"{r['ticker']:<10} {r['name']:<14} {r['profit_pct']:>7.1f}% "
|
||||
f"{r['profit_lock_stage']:<20} {r['velocity_1d']:>12.2f}% "
|
||||
f"{r['anti_chasing_verdict']:<15} {ts_str}"
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
# 매도가 역전 경보
|
||||
invalid_prices = [r for r in per_ticker if r.get("sell_price_sanity_status", "PASS") != "PASS"]
|
||||
if invalid_prices:
|
||||
print(f"[!] SELL_PRICE_SANITY 경보 — {len(invalid_prices)}개 종목 HTS 입력 차단")
|
||||
for r in invalid_prices:
|
||||
for issue in r.get("sell_price_sanity_issues", []):
|
||||
print(f" {r['ticker']} {r['name']}: {issue}")
|
||||
else:
|
||||
print("[✔] SELL_PRICE_SANITY — 전 종목 가격 정상")
|
||||
|
||||
# 현금회복 계획
|
||||
crp = computed.get("cash_recovery_plan_json")
|
||||
if crp:
|
||||
print(f"\n[현금회복 최적 매도조합] 부족분 {cash_shortfall:,.0f}원")
|
||||
print(f" {'#':<3} {'ticker':<10} {'종목명':<14} {'수량':>6} {'지정가':>10} {'예상회수':>12}")
|
||||
print(" " + "-" * 58)
|
||||
for i, s in enumerate(crp.get("sell_sequence", []), 1):
|
||||
lp = s.get("limit_price") or 0
|
||||
ek = s.get("expected_krw") or 0
|
||||
print(f" {i:<3} {s['ticker']:<10} {s['name']:<14} {s.get('qty', 0):>6} "
|
||||
f"{lp:>10,} {ek:>12,}")
|
||||
met_str = "✔ 달성" if crp.get("shortfall_met") else "✗ 부족"
|
||||
print(f" 합계: {crp.get('expected_total_krw', 0):>12,}원 ({met_str})")
|
||||
|
||||
# ── JSON 파일 저장 ─────────────────────────────────────────────────────
|
||||
if output_path is None:
|
||||
output_path = ROOT / "Temp" / "computed_harness.json"
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(
|
||||
json.dumps(computed, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
print(f"\n → 결과 저장: {output_path}")
|
||||
print(f" → 결정론적 계산 완료. LLM 추정 없음.\n")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# IMPUTED_DATA_EXPOSURE_GATE_V1
|
||||
# weighted_coverage = Σ(weight × coverage); ifr = 1 - wc; ech = raw × (0.4 + 0.6 × wc)
|
||||
# gate: ifr ≥ 0.50 → BLOCK / ≥ 0.25 → WARN / else PASS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
_IDEG_WEIGHTS = {"fundamental_core": 0.30, "realized_outcome": 0.30,
|
||||
"trade_quality": 0.15, "pattern": 0.10, "alpha_eval": 0.15}
|
||||
|
||||
|
||||
def compute_imputed_data_exposure(domain_coverage: dict, raw_cap: float) -> dict:
|
||||
wc = sum(_IDEG_WEIGHTS.get(k, 0) * v for k, v in domain_coverage.items())
|
||||
ifr = round(1.0 - wc, 4)
|
||||
ech = round(raw_cap * (0.4 + 0.6 * wc), 1)
|
||||
gate = "IMPUTED_DATA_BLOCK" if ifr >= 0.50 else ("IMPUTED_DATA_WARN" if ifr >= 0.25 else "PASS")
|
||||
return {"gate_status": gate, "imputed_field_ratio": round(ifr, 4),
|
||||
"weighted_coverage": round(wc, 4), "effective_confidence_honest": ech}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# TRAILING_STOP_PRICE_V1
|
||||
# trailing_stop = highest_price - atr20 × multiplier
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_trailing_stop_price(highest_price_since_entry: float, atr20: float,
|
||||
trailing_atr_multiplier: float) -> dict:
|
||||
return {"trailing_stop_price": round(highest_price_since_entry - atr20 * trailing_atr_multiplier)}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# EXPECTED_EDGE_V1
|
||||
# edge = ((tp - entry) / (entry - stop)) × confidence
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_expected_edge(target_price: float, entry_price: float,
|
||||
stop_price: float, bayesian_confidence: float) -> dict:
|
||||
r_multiple = (target_price - entry_price) / (entry_price - stop_price) if (entry_price - stop_price) != 0 else 0
|
||||
edge = round(r_multiple * bayesian_confidence, 4)
|
||||
return {"expected_edge": edge, "r_multiple": round(r_multiple, 4)}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# TP_VALIDITY_CHECK_V1
|
||||
# tp_price > current_price → valid; else null (triggered)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_tp_validity(tp_price: float | None, current_price: float) -> dict:
|
||||
if tp_price is None:
|
||||
return {"tp_validated_price": None, "tp_state": "UNKNOWN_NO_CLOSE"}
|
||||
if tp_price > current_price:
|
||||
return {"tp_validated_price": tp_price, "tp_state": "PENDING"}
|
||||
return {"tp_validated_price": None, "tp_state": "TP1_ALREADY_TRIGGERED"}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# RS_RATIO_V1
|
||||
# rs_ratio = stock_5d_return / kospi_5d_return
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_rs_ratio(stock_close_5d_return: float, kospi_close_5d_return: float) -> dict:
|
||||
if kospi_close_5d_return == 0:
|
||||
return {"rs_ratio": None, "note": "kospi_return=0"}
|
||||
return {"rs_ratio": round(stock_close_5d_return / kospi_close_5d_return, 4)}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# RATCHET_TRAILING_AUTO_V1
|
||||
# PROFIT_LOCK_20: max(ratchet_stop, highest_close - 1.5×ATR20)
|
||||
# PROFIT_LOCK_30/APEX: max(ratchet_stop, highest_close - 2.0×ATR20)
|
||||
# Others: null
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_ratchet_trailing_auto(profit_lock_stage: str, ratchet_stop: float,
|
||||
highest_close: float, atr20: float) -> dict:
|
||||
_mult = {"PROFIT_LOCK_20": 1.5, "PROFIT_LOCK_30": 2.0, "APEX_TRAILING": 2.0}
|
||||
m = _mult.get(profit_lock_stage)
|
||||
if m is None:
|
||||
return {"auto_trailing_stop": None, "note": f"{profit_lock_stage}: no trailing"}
|
||||
atr_stop = highest_close - m * atr20
|
||||
result = max(ratchet_stop, atr_stop)
|
||||
return {"auto_trailing_stop": round(result), "atr_stop": round(atr_stop),
|
||||
"ratchet_stop": ratchet_stop, "multiplier": m}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STOP_PRICE_CORE_V1
|
||||
# max(entry × 0.92, entry - atr20 × multiplier)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_stop_price_core(entry_price: float, atr20: float, atr_multiplier: float) -> dict:
|
||||
return _compute_stop_price_core(entry_price, atr20, atr_multiplier=atr_multiplier)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# TARGET_CASH_PCT_V1
|
||||
# max(5 + (mrs/10)×15, cash_floor_regime_min_pct)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_target_cash_pct(market_risk_score: float, cash_floor_regime_min_pct: float) -> dict:
|
||||
formula_result = 5 + (market_risk_score / 10) * 15
|
||||
target = max(formula_result, cash_floor_regime_min_pct)
|
||||
return {"target_cash_pct": round(target, 2), "formula_result": round(formula_result, 2)}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# FLOW_CREDIT_V1
|
||||
# C1×0.30 + C2×0.30 + C3×0.40
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_flow_credit(c1_price_action: float, c2_volume_action: float,
|
||||
c3_flow_action: float) -> dict:
|
||||
credit = round(c1_price_action * 0.30 + c2_volume_action * 0.30 + c3_flow_action * 0.40, 4)
|
||||
return {"flow_credit": credit}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# MARKET_RISK_SCORE_V1
|
||||
# min(10, vix + kospi + usd_krw + usd_jpy + credit)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_market_risk_score(vix_score: float, kospi_score: float, usd_krw_score: float,
|
||||
usd_jpy_score: float, credit_score: float) -> dict:
|
||||
raw = vix_score + kospi_score + usd_krw_score + usd_jpy_score + credit_score
|
||||
return {"market_risk_score": min(10, raw), "raw_sum": raw}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PORTFOLIO_BETA_V1
|
||||
# beta = Σ(beta_i × mv_i) / total_equity
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_portfolio_beta(holdings: list, total_equity_value: float) -> dict:
|
||||
if total_equity_value <= 0:
|
||||
return {"portfolio_beta": None}
|
||||
weighted = sum(h.get("beta", 0) * h.get("market_value", 0) for h in holdings
|
||||
if h.get("beta") is not None)
|
||||
return {"portfolio_beta": round(weighted / total_equity_value, 4)}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# RISK_BUDGET_CASCADE_V1
|
||||
# base × feedback_mult × brake_mult
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_risk_budget_cascade(base_risk_budget: float, net_return_feedback_multiplier: float,
|
||||
performance_brake_multiplier: float) -> dict:
|
||||
result = base_risk_budget * net_return_feedback_multiplier * performance_brake_multiplier
|
||||
return {"risk_budget": round(result, 6)}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# MEAN_REVERSION_GATE_V1
|
||||
# deviation_ratio = close / ma20; gate by ratio
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_mean_reversion_gate(close_price: float, ma20: float) -> dict:
|
||||
if ma20 <= 0:
|
||||
return {"deviation_ratio": None, "gate": "DATA_MISSING"}
|
||||
ratio = round(close_price / ma20, 4)
|
||||
gate = "OVEREXTENDED" if ratio >= 1.10 else ("NORMAL" if ratio >= 0.95 else "OVERSOLD")
|
||||
return {"deviation_ratio": ratio, "gate": gate}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# T1_FORCED_SELL_RISK_V1
|
||||
# min(100, sell_action_active*40 + timing_exit_ge_50*25 + rw_ge_2*25 + dist_ge_70*30)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_t1_forced_sell_risk(sell_action_active: int, timing_exit_ge_50: int,
|
||||
rw_ge_2: int, distribution_ge_70: int) -> dict:
|
||||
score = min(100, sell_action_active * 40 + timing_exit_ge_50 * 25 +
|
||||
rw_ge_2 * 25 + distribution_ge_70 * 30)
|
||||
gate = "HIGH_SELL_RISK" if score >= 70 else ("MODERATE" if score >= 40 else "LOW")
|
||||
return {"t1_forced_sell_risk_score": score, "gate": gate}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# SELL_CONFLICT_AWARE_RECOMMENDATION_V1
|
||||
# min(100, sell_signal_active*55 + cash_preserve_active*20 + no_add_gate*20)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_sell_conflict_recommendation(sell_signal_active: int, cash_preserve_active: int,
|
||||
no_add_gate: int) -> dict:
|
||||
score = min(100, sell_signal_active * 55 + cash_preserve_active * 20 + no_add_gate * 20)
|
||||
recommendation = "SELL_PRIORITY" if score >= 55 else ("REDUCE" if score >= 20 else "HOLD")
|
||||
return {"conflict_score": score, "recommendation": recommendation}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# DIVERGENCE_SCORE_V1
|
||||
# divergence = price_above_ma20 × (frg_sell×0.40 + inst_sell×0.35 + vol_surge×0.25)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_divergence_score(price_above_ma20: int, foreign_net_sell: float,
|
||||
institution_net_sell: float, vol_surge: float) -> dict:
|
||||
score = price_above_ma20 * (foreign_net_sell * 0.40 + institution_net_sell * 0.35 +
|
||||
vol_surge * 0.25)
|
||||
score = round(min(100, max(-100, score)), 2)
|
||||
gate = "DISTRIBUTION_SIGNAL" if score >= 50 else ("NEUTRAL" if score >= 0 else "ACCUMULATION")
|
||||
return {"divergence_score": score, "gate": gate}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# SEMICONDUCTOR_CLUSTER_SYNC_V1
|
||||
# is_mandatory = cluster_pct > cluster_limit_pct × 2
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_semiconductor_cluster_sync(cluster_pct: float, cluster_limit_pct: float) -> dict:
|
||||
is_mandatory = cluster_pct > cluster_limit_pct * 2
|
||||
ratio = round(cluster_pct / cluster_limit_pct, 3) if cluster_limit_pct > 0 else None
|
||||
gate = "MANDATORY_REDUCE" if is_mandatory else "PASS"
|
||||
return {"is_mandatory": is_mandatory, "cluster_ratio": ratio, "gate": gate}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GOAL_RETIREMENT_V1
|
||||
# achievement_pct = round(total_asset / GOAL_KRW * 1000) / 10
|
||||
# goal_remaining_krw = max(0, GOAL_KRW - total_asset)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_goal_retirement(total_asset_krw: float, goal_krw: float = 500_000_000) -> dict:
|
||||
achievement_pct = round(total_asset_krw / goal_krw * 1000) / 10
|
||||
remaining_krw = max(0.0, goal_krw - total_asset_krw)
|
||||
status = "ACHIEVED" if achievement_pct >= 100 else "IN_PROGRESS"
|
||||
return {
|
||||
"goal_achievement_pct": achievement_pct,
|
||||
"goal_remaining_krw": round(remaining_krw),
|
||||
"goal_status": status,
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# VELOCITY_1D_V1 (renamed from VELOCITY_V1 for clarity)
|
||||
# velocity = (close - prev_close) / prev_close × 100
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_velocity_1d(close: float, prev_close: float) -> dict:
|
||||
if prev_close <= 0:
|
||||
return {"velocity_1d": None}
|
||||
v = round((close - prev_close) / prev_close * 100, 4)
|
||||
return {"velocity_1d": v}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# POSITION_SIZE_V1
|
||||
# position_size = min(atr_qty, cash_limit_qty, weight_limit_qty, sector_limit_qty)
|
||||
# atr_qty = floor(risk_budget_krw / (atr20 × atr_mult × close))
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def compute_position_size(risk_budget_krw: float, atr20: float, atr_mult: float,
|
||||
close: float, cash_limit_qty: int,
|
||||
weight_limit_qty: int, sector_limit_qty: int) -> dict:
|
||||
import math
|
||||
if atr20 <= 0 or close <= 0 or atr_mult <= 0:
|
||||
return {"position_size_qty": 0, "binding_constraint": "DATA_MISSING"}
|
||||
atr_qty = math.floor(risk_budget_krw / (atr20 * atr_mult * close))
|
||||
constraints = {
|
||||
"atr": atr_qty, "cash": cash_limit_qty,
|
||||
"weight": weight_limit_qty, "sector": sector_limit_qty
|
||||
}
|
||||
min_val = min(constraints.values())
|
||||
binding = [k for k, v in constraints.items() if v == min_val][0]
|
||||
return {"position_size_qty": max(0, min_val), "atr_qty": atr_qty,
|
||||
"binding_constraint": binding, "constraints": constraints}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,564 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
|
||||
KRX_TICK_TABLE: tuple[tuple[float, int], ...] = (
|
||||
(2_000, 1),
|
||||
(5_000, 5),
|
||||
(20_000, 10),
|
||||
(50_000, 50),
|
||||
(200_000, 100),
|
||||
(500_000, 500),
|
||||
(math.inf, 1000),
|
||||
)
|
||||
|
||||
|
||||
def krx_tick_unit(price: float) -> int:
|
||||
for threshold, tick in KRX_TICK_TABLE:
|
||||
if price < threshold:
|
||||
return tick
|
||||
return 1000
|
||||
|
||||
|
||||
def normalize_tick(price: float) -> int:
|
||||
tick = krx_tick_unit(price)
|
||||
return int(math.floor(price / tick) * tick)
|
||||
|
||||
|
||||
def compute_stop_price_core(
|
||||
entry_price: float | None,
|
||||
atr20: float | None,
|
||||
current_price: float | None = None,
|
||||
atr_multiplier: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if entry_price is None:
|
||||
return {
|
||||
"stop_price": None,
|
||||
"stop_price_status": "NO_STOP_PRICE",
|
||||
"data_missing": ["entry_price"],
|
||||
}
|
||||
|
||||
if atr20 is None and atr_multiplier is None:
|
||||
return {
|
||||
"stop_price": entry_price * 0.92,
|
||||
"stop_price_status": "DATA_MISSING — 하네스 업데이트 필요",
|
||||
"data_missing": ["atr20"],
|
||||
}
|
||||
|
||||
if atr_multiplier is None and current_price in (None, 0):
|
||||
return {
|
||||
"stop_price": entry_price * 0.92,
|
||||
"stop_price_status": "DATA_MISSING — 하네스 업데이트 필요",
|
||||
"data_missing": [name for name, value in (("atr20", atr20), ("current_price", current_price)) if value in (None, 0)],
|
||||
}
|
||||
|
||||
if atr_multiplier is None:
|
||||
atr20_pct = (atr20 / current_price) * 100
|
||||
atr_multiplier = 2.0 if atr20_pct >= 8 else 1.5
|
||||
else:
|
||||
atr20_pct = (atr20 / current_price) * 100 if current_price not in (None, 0) else None
|
||||
return {
|
||||
"stop_price": max(entry_price * 0.92, entry_price - atr20 * atr_multiplier),
|
||||
"stop_price_status": "PASS",
|
||||
"atr20_pct": atr20_pct,
|
||||
"atr_multiplier": atr_multiplier,
|
||||
}
|
||||
|
||||
|
||||
def compute_stop_action_ladder(context: dict[str, Any]) -> dict[str, Any]:
|
||||
timing_action = str(context.get("timingAction") or context.get("timing_action") or "").upper()
|
||||
rw_partial = int(context.get("rw_partial") or 0)
|
||||
rw_partial_excluding_rw2b = int(context.get("rw_partial_excluding_rw2b") or 0)
|
||||
regime_prelim = str(context.get("REGIME_PRELIM") or context.get("regime_prelim") or "").upper()
|
||||
timing_exit_score = float(context.get("timingExitScore") or context.get("timing_exit_score") or 0.0)
|
||||
profit_pct = float(context.get("profitPct") or context.get("profit_pct") or 0.0)
|
||||
days_to_time_stop = int(context.get("daysToTimeStop") or context.get("days_to_time_stop") or 9999)
|
||||
trailing_stop_breach = bool(context.get("trailingStopBreach") or context.get("trailing_stop_breach") or False)
|
||||
rw2b_fast_track = bool(context.get("RW2b_5d_rapid_weakness") or context.get("rw2b_5d_rapid_weakness") or False)
|
||||
|
||||
if timing_action == "STOP_OR_TIME_EXIT_READY" or rw_partial >= 4:
|
||||
return {
|
||||
"action": "EXIT_100",
|
||||
"reason": "STOP_OR_TIME_EXIT_READY" if timing_action == "STOP_OR_TIME_EXIT_READY" else "RW_EXIT_STRONG",
|
||||
"quantity_pct": 100,
|
||||
"priority": 1,
|
||||
}
|
||||
if regime_prelim in {"RISK_OFF", "RISK_OFF_CANDIDATE"}:
|
||||
return {
|
||||
"action": "REGIME_TRIM_50",
|
||||
"reason": "REGIME_RISK_OFF" if regime_prelim == "RISK_OFF" else "REGIME_RISK_OFF_CANDIDATE",
|
||||
"quantity_pct": 50,
|
||||
"priority": 2,
|
||||
}
|
||||
if rw2b_fast_track and rw_partial_excluding_rw2b >= 1:
|
||||
return {
|
||||
"action": "TRIM_50",
|
||||
"reason": "RW2B_FAST_TRACK",
|
||||
"quantity_pct": 50,
|
||||
"priority": 2.5,
|
||||
}
|
||||
if rw_partial >= 3 or timing_exit_score >= 75:
|
||||
return {
|
||||
"action": "TRIM_70",
|
||||
"reason": "RW_EXIT" if rw_partial >= 3 else "TIMING_EXIT_SCORE",
|
||||
"quantity_pct": 70,
|
||||
"priority": 3,
|
||||
}
|
||||
if trailing_stop_breach or rw_partial >= 2 or (rw_partial >= 1 and timing_exit_score >= 50):
|
||||
return {
|
||||
"action": "TRIM_50",
|
||||
"reason": "TRAILING_STOP_BREACH" if trailing_stop_breach else "RW_OR_TIMING_EXIT",
|
||||
"quantity_pct": 50,
|
||||
"priority": 4,
|
||||
}
|
||||
if profit_pct >= 10:
|
||||
return {
|
||||
"action": "TAKE_PROFIT_TIER1",
|
||||
"reason": "PROFIT_PCT_THRESHOLD",
|
||||
"quantity_pct": 25,
|
||||
"priority": 5,
|
||||
}
|
||||
if days_to_time_stop <= 0:
|
||||
return {
|
||||
"action": "TIME_EXIT_100",
|
||||
"reason": "TIME_STOP_EXPIRED",
|
||||
"quantity_pct": 100,
|
||||
"priority": 6,
|
||||
}
|
||||
return {
|
||||
"action": "REVIEW_HUMAN",
|
||||
"reason": "NO_FORCED_EXIT",
|
||||
"quantity_pct": 0,
|
||||
"priority": 7,
|
||||
}
|
||||
|
||||
|
||||
def compute_dynamic_heat_thresholds(regime: str | None) -> dict[str, float]:
|
||||
r = str(regime or "").upper()
|
||||
if "EVENT_SHOCK" in r:
|
||||
return {"hardBlock": 5.0, "halve": 3.5}
|
||||
if "RISK_OFF" in r:
|
||||
return {"hardBlock": 7.0, "halve": 5.0}
|
||||
if "SECULAR_LEADER" in r and "RISK_ON" in r:
|
||||
return {"hardBlock": 13.0, "halve": 9.0}
|
||||
if "RISK_ON" in r:
|
||||
return {"hardBlock": 12.0, "halve": 8.5}
|
||||
return {"hardBlock": 10.0, "halve": 7.0}
|
||||
|
||||
|
||||
def compute_cash_shortfall_harness(
|
||||
as_result: dict[str, Any],
|
||||
total_asset: float,
|
||||
cash_floor_info: dict[str, Any],
|
||||
mrs_score: float,
|
||||
) -> dict[str, Any]:
|
||||
asset = total_asset if math.isfinite(total_asset) and total_asset > 0 else 0.0
|
||||
d2_krw = float(as_result.get("settlementCashD2Krw") or 0.0)
|
||||
min_pct = float(cash_floor_info.get("minPct") or 0.0)
|
||||
target_cash_pct = max(5 + (mrs_score / 10) * 15, min_pct)
|
||||
return {
|
||||
"cash_current_pct_d2": round((d2_krw / asset * 100), 2) if asset > 0 else 0,
|
||||
"cash_target_pct": target_cash_pct,
|
||||
"cash_shortfall_min_krw": max(0, round(asset * min_pct / 100 - d2_krw)),
|
||||
"cash_shortfall_target_krw": max(0, round(asset * target_cash_pct / 100 - d2_krw)),
|
||||
}
|
||||
|
||||
|
||||
def compute_timing_decision(ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
reasons: list[str] = []
|
||||
entry_score = 0
|
||||
exit_score = 0
|
||||
|
||||
entry_gate = str(ctx.get("entryModeGate") or "")
|
||||
entry_mode = str(ctx.get("entryMode") or "")
|
||||
leader_gate = str(ctx.get("leaderGate") or "")
|
||||
ac_gate = str(ctx.get("acGate") or "")
|
||||
exit_signal = str(ctx.get("exitSignalDetail") or "")
|
||||
flow_credit = ctx.get("flowCredit")
|
||||
leader_total = ctx.get("leaderTotal")
|
||||
rw_partial = ctx.get("rwPartial")
|
||||
rsi14 = ctx.get("rsi14")
|
||||
disparity = ctx.get("disparity")
|
||||
ma20_slope = ctx.get("ma20Slope")
|
||||
spread_pct = ctx.get("spreadPct")
|
||||
avg_trade_value_5d = ctx.get("avgTradeValue5D")
|
||||
profit_pct = ctx.get("profitPct")
|
||||
days_to_time_stop = ctx.get("daysToTimeStop")
|
||||
|
||||
if entry_gate == "PASS":
|
||||
entry_score += 25
|
||||
reasons.append(f"entry_{entry_mode}")
|
||||
elif entry_gate == "BLOCK":
|
||||
entry_score -= 25
|
||||
reasons.append("entry_block")
|
||||
|
||||
if isinstance(leader_total, (int, float)) and math.isfinite(float(leader_total)):
|
||||
if leader_total >= 4:
|
||||
entry_score += 20
|
||||
reasons.append("leader_scan>=4")
|
||||
elif leader_total >= 3:
|
||||
entry_score += 10
|
||||
reasons.append("leader_watch")
|
||||
if leader_gate in {"PASS", "EXPLORE_CANDIDATE"}:
|
||||
entry_score += 10
|
||||
|
||||
if isinstance(flow_credit, (int, float)) and math.isfinite(float(flow_credit)):
|
||||
if flow_credit >= 0.7:
|
||||
entry_score += 20
|
||||
reasons.append("flow_strong")
|
||||
elif flow_credit >= 0.4:
|
||||
entry_score += 10
|
||||
reasons.append("flow_partial")
|
||||
|
||||
if ac_gate == "CLEAR":
|
||||
entry_score += 15
|
||||
reasons.append("anti_climax_clear")
|
||||
elif ac_gate == "CAUTION":
|
||||
entry_score += 5
|
||||
reasons.append("anti_climax_caution")
|
||||
elif ac_gate == "BLOCK":
|
||||
entry_score -= 35
|
||||
exit_score += 15
|
||||
reasons.append("anti_climax_block")
|
||||
|
||||
if isinstance(ma20_slope, (int, float)) and math.isfinite(float(ma20_slope)):
|
||||
if ma20_slope > 0:
|
||||
entry_score += 8
|
||||
else:
|
||||
entry_score -= 8
|
||||
exit_score += 8
|
||||
reasons.append("ma20_down")
|
||||
if isinstance(disparity, (int, float)) and math.isfinite(float(disparity)):
|
||||
if -5 <= disparity <= 4:
|
||||
entry_score += 10
|
||||
elif 4 < disparity <= 8:
|
||||
entry_score += 5
|
||||
elif disparity > 12:
|
||||
entry_score -= 25
|
||||
exit_score += 20
|
||||
reasons.append("overextended")
|
||||
elif disparity < -10:
|
||||
entry_score -= 10
|
||||
exit_score += 10
|
||||
reasons.append("trend_damage")
|
||||
if isinstance(rsi14, (int, float)) and math.isfinite(float(rsi14)):
|
||||
if 40 <= rsi14 <= 65:
|
||||
entry_score += 10
|
||||
elif 65 < rsi14 <= 72:
|
||||
entry_score += 4
|
||||
elif rsi14 > 75:
|
||||
entry_score -= 25
|
||||
exit_score += 20
|
||||
reasons.append("rsi_overbought")
|
||||
elif rsi14 < 35:
|
||||
entry_score -= 5
|
||||
exit_score += 8
|
||||
reasons.append("weak_rsi")
|
||||
if (
|
||||
isinstance(avg_trade_value_5d, (int, float))
|
||||
and math.isfinite(float(avg_trade_value_5d))
|
||||
and avg_trade_value_5d >= 50
|
||||
and (not isinstance(spread_pct, (int, float)) or not math.isfinite(float(spread_pct)) or spread_pct <= 0.8)
|
||||
):
|
||||
entry_score += 10
|
||||
else:
|
||||
entry_score -= 15
|
||||
reasons.append("liquidity_or_spread_fail")
|
||||
|
||||
if isinstance(rw_partial, (int, float)) and math.isfinite(float(rw_partial)):
|
||||
exit_score += min(100, max(0, int(rw_partial) * 25))
|
||||
if exit_signal:
|
||||
exit_score += len([part for part in exit_signal.split("|") if part]) * 10
|
||||
if isinstance(days_to_time_stop, (int, float)) and 0 <= float(days_to_time_stop) <= 7:
|
||||
exit_score += 20
|
||||
reasons.append("time_stop_near")
|
||||
if isinstance(profit_pct, (int, float)) and profit_pct >= 10:
|
||||
exit_score += 15
|
||||
reasons.append("profit_protect_zone")
|
||||
|
||||
entry_score = max(0, min(100, round(entry_score)))
|
||||
exit_score = max(0, min(100, round(exit_score)))
|
||||
|
||||
action = "HOLD_NO_TIMING_EDGE"
|
||||
atr20 = ctx.get("atr20")
|
||||
price_status = str(ctx.get("priceStatus") or "")
|
||||
if price_status != "PRICE_OK" or not isinstance(atr20, (int, float)) or not math.isfinite(float(atr20)):
|
||||
action = "OBSERVE_DATA_MISSING"
|
||||
elif exit_score >= 75 or (isinstance(rw_partial, (int, float)) and rw_partial >= 4):
|
||||
action = "STOP_OR_TIME_EXIT_READY"
|
||||
elif exit_score >= 50 or (isinstance(rw_partial, (int, float)) and rw_partial >= 3):
|
||||
action = "EXIT_REVIEW"
|
||||
elif entry_gate == "BLOCK" or ac_gate == "BLOCK" or entry_mode == "OVERBOUGHT":
|
||||
action = "NO_BUY_OVERHEATED"
|
||||
elif entry_score >= 75 and entry_gate == "PASS" and isinstance(leader_total, (int, float)) and leader_total >= 4:
|
||||
action = "BUY_BREAKOUT_PILOT_ONLY" if entry_mode == "BREAKOUT" else "BUY_STAGE1_READY"
|
||||
elif entry_score >= 60 and entry_gate == "PASS":
|
||||
action = "BUY_BREAKOUT_PILOT_ONLY" if entry_mode == "BREAKOUT" else "BUY_PULLBACK_WAIT"
|
||||
elif (
|
||||
(isinstance(leader_total, (int, float)) and leader_total >= 3)
|
||||
or (isinstance(flow_credit, (int, float)) and flow_credit >= 0.4)
|
||||
):
|
||||
action = "WATCH_TIMING_SETUP"
|
||||
|
||||
return {
|
||||
"entry_score": entry_score,
|
||||
"exit_score": exit_score,
|
||||
"action": action,
|
||||
"reason": "|".join(reasons[:6]),
|
||||
}
|
||||
|
||||
|
||||
def compute_sell_decision(ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
close = ctx.get("close")
|
||||
stop_price = ctx.get("stopPrice")
|
||||
trailing_stop = ctx.get("trailingStop")
|
||||
tp1_price = ctx.get("tp1Price")
|
||||
tp2_price = ctx.get("tp2Price")
|
||||
profit_pct = ctx.get("profitPct")
|
||||
rw_partial = ctx.get("rwPartial")
|
||||
timing_exit_score = ctx.get("timingExitScore")
|
||||
days_to_time_stop = ctx.get("daysToTimeStop")
|
||||
timing_action = str(ctx.get("timingAction") or "")
|
||||
regime = str(ctx.get("regime") or "")
|
||||
atr20 = ctx.get("atr20")
|
||||
|
||||
close_f = float(close) if isinstance(close, (int, float)) else float("nan")
|
||||
stop_f = float(stop_price) if isinstance(stop_price, (int, float)) else float("nan")
|
||||
trailing_f = float(trailing_stop) if isinstance(trailing_stop, (int, float)) else float("nan")
|
||||
tp1_f = float(tp1_price) if isinstance(tp1_price, (int, float)) else float("nan")
|
||||
tp2_f = float(tp2_price) if isinstance(tp2_price, (int, float)) else float("nan")
|
||||
profit_f = float(profit_pct) if isinstance(profit_pct, (int, float)) else float("nan")
|
||||
rw_f = float(rw_partial) if isinstance(rw_partial, (int, float)) else float("nan")
|
||||
timing_exit_f = float(timing_exit_score) if isinstance(timing_exit_score, (int, float)) else float("nan")
|
||||
days_f = float(days_to_time_stop) if isinstance(days_to_time_stop, (int, float)) else float("nan")
|
||||
atr_f = float(atr20) if isinstance(atr20, (int, float)) else float("nan")
|
||||
|
||||
action = "HOLD"
|
||||
ratio = 0
|
||||
reason = ""
|
||||
price: Any = ""
|
||||
price_source = ""
|
||||
price_basis = ""
|
||||
execution_window = ""
|
||||
order_type = ""
|
||||
|
||||
stop_candidate = trailing_f if math.isfinite(trailing_f) and trailing_f > 0 else stop_f
|
||||
if not (math.isfinite(stop_candidate) and stop_candidate > 0) and math.isfinite(close_f) and close_f > 0:
|
||||
stop_candidate = close_f * 0.995
|
||||
protective_limit = round(min(close_f * 0.995, stop_candidate if math.isfinite(stop_candidate) else close_f * 0.995)) if math.isfinite(close_f) and close_f > 0 else ""
|
||||
atr_buffer = atr_f * 0.3 if math.isfinite(atr_f) and atr_f > 0 else (close_f * 0.005 if math.isfinite(close_f) else 0)
|
||||
close_protect_limit = round(close_f - atr_buffer) if math.isfinite(close_f) and close_f > 0 else ""
|
||||
|
||||
if timing_action == "STOP_OR_TIME_EXIT_READY" or (math.isfinite(rw_f) and rw_f >= 4):
|
||||
action = "EXIT_100"
|
||||
ratio = 100
|
||||
reason = "RW_EXIT_STRONG" if math.isfinite(rw_f) and rw_f >= 4 else "STOP_OR_TIME_EXIT_READY"
|
||||
price = protective_limit
|
||||
price_source = "TRAILING_STOP" if math.isfinite(trailing_f) else "STOP_OR_CLOSE"
|
||||
price_basis = "TRAILING_STOP_TRIGGER" if math.isfinite(trailing_f) else "STOP_OR_CLOSE_PROTECT"
|
||||
execution_window = "INTRADAY_ON_TRIGGER"
|
||||
order_type = "PROTECTIVE_LIMIT_SELL"
|
||||
elif math.isfinite(rw_f) and rw_f >= 3 or (math.isfinite(timing_exit_f) and timing_exit_f >= 75):
|
||||
action = "TRIM_70"
|
||||
ratio = 70
|
||||
reason = "RW_EXIT" if math.isfinite(rw_f) and rw_f >= 3 else "TIMING_EXIT_SCORE"
|
||||
price = protective_limit
|
||||
price_source = "RISK_REDUCTION"
|
||||
price_basis = "RISK_REDUCTION_CLOSE_PROTECT"
|
||||
execution_window = "INTRADAY_AFTER_09_30"
|
||||
order_type = "PROTECTIVE_LIMIT_SELL"
|
||||
elif math.isfinite(trailing_f) and trailing_f > 0 and math.isfinite(close_f) and close_f <= trailing_f:
|
||||
action = "TRAILING_STOP_BREACH"
|
||||
ratio = 70
|
||||
reason = "TRAILING_STOP_PRICE_BREACH"
|
||||
price = round(trailing_f)
|
||||
price_source = "TRAILING_STOP_PRICE"
|
||||
price_basis = "TRAILING_STOP_TRIGGER"
|
||||
execution_window = "INTRADAY_ON_TRIGGER"
|
||||
order_type = "PROTECTIVE_LIMIT_SELL"
|
||||
elif (math.isfinite(rw_f) and rw_f >= 2) or (math.isfinite(rw_f) and rw_f >= 1 and math.isfinite(timing_exit_f) and timing_exit_f >= 50):
|
||||
action = "TRIM_50"
|
||||
ratio = 50
|
||||
reason = "RW_REVIEW" if math.isfinite(rw_f) and rw_f >= 2 else "TIMING_EXIT_REVIEW"
|
||||
price = close_protect_limit
|
||||
price_source = "RELATIVE_WEAKNESS_CLOSE"
|
||||
price_basis = "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_AFTER_09_30"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif math.isfinite(rw_f) and rw_f >= 1 and math.isfinite(timing_exit_f) and timing_exit_f >= 30:
|
||||
action = "TRIM_33"
|
||||
ratio = 33
|
||||
reason = "RW_EARLY_WARNING"
|
||||
price = close_protect_limit
|
||||
price_source = "EARLY_WARNING_CLOSE"
|
||||
price_basis = "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_AFTER_09_30"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif math.isfinite(rw_f) and rw_f >= 1:
|
||||
action = "TRIM_25"
|
||||
ratio = 25
|
||||
reason = "RW_SIGNAL_ONLY"
|
||||
price = close_protect_limit
|
||||
price_source = "SIGNAL_ONLY_CLOSE"
|
||||
price_basis = "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif math.isfinite(profit_f) and profit_f >= 50:
|
||||
action = "PROFIT_TRIM_50"
|
||||
ratio = 50
|
||||
reason = "PROFIT_PROTECT_50"
|
||||
price = round(tp2_f) if math.isfinite(tp2_f) and tp2_f > 0 else close_protect_limit
|
||||
price_source = "TP2_PRICE" if math.isfinite(tp2_f) else "CLOSE_PROFIT_PROTECT"
|
||||
price_basis = "TAKE_PROFIT_TIER2_PRICE" if math.isfinite(tp2_f) else "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif math.isfinite(profit_f) and profit_f >= 30:
|
||||
action = "PROFIT_TRIM_35"
|
||||
ratio = 35
|
||||
reason = "PROFIT_PROTECT_30"
|
||||
price = round(tp2_f) if math.isfinite(tp2_f) and tp2_f > 0 else close_protect_limit
|
||||
price_source = "TP2_PRICE" if math.isfinite(tp2_f) else "CLOSE_PROFIT_PROTECT"
|
||||
price_basis = "TAKE_PROFIT_TIER2_PRICE" if math.isfinite(tp2_f) else "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif math.isfinite(profit_f) and profit_f >= 20:
|
||||
action = "PROFIT_TRIM_25"
|
||||
ratio = 25
|
||||
reason = "PROFIT_PROTECT_20"
|
||||
price = round(tp1_f) if math.isfinite(tp1_f) and tp1_f > 0 else close_protect_limit
|
||||
price_source = "TP1_PRICE" if math.isfinite(tp1_f) else "CLOSE_PROFIT_PROTECT"
|
||||
price_basis = "TAKE_PROFIT_TIER1_PRICE" if math.isfinite(tp1_f) else "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif math.isfinite(profit_f) and profit_f >= 10:
|
||||
action = "TAKE_PROFIT_TIER1"
|
||||
ratio = 25
|
||||
reason = "TP1_PROFIT_10PCT"
|
||||
price = round(tp1_f) if math.isfinite(tp1_f) and tp1_f > 0 else close_protect_limit
|
||||
price_source = "TP1_PRICE" if math.isfinite(tp1_f) else "CLOSE_PROFIT_PROTECT"
|
||||
price_basis = "TAKE_PROFIT_TIER1_PRICE" if math.isfinite(tp1_f) else "PRIOR_CLOSE_X_0.998"
|
||||
execution_window = "INTRADAY_LIMIT_OR_CLOSE_REVIEW"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif math.isfinite(days_f) and days_f <= 0:
|
||||
action = "TIME_EXIT_100"
|
||||
ratio = 100
|
||||
reason = "TIME_STOP_EXPIRED"
|
||||
price = protective_limit
|
||||
price_source = "TIME_STOP_CLOSE"
|
||||
price_basis = "TIME_STOP_CLOSE_PROTECT"
|
||||
execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
|
||||
order_type = "PROTECTIVE_LIMIT_SELL"
|
||||
elif math.isfinite(days_f) and days_f <= 7:
|
||||
action = "TIME_TRIM_50"
|
||||
ratio = 50
|
||||
reason = "TIME_STOP_NEAR"
|
||||
price = close_protect_limit
|
||||
price_source = "TIME_STOP_NEAR_CLOSE"
|
||||
price_basis = "ATR_PROTECT_LIMIT"
|
||||
execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
|
||||
order_type = "LIMIT_SELL"
|
||||
elif math.isfinite(days_f) and days_f <= 14:
|
||||
action = "TIME_TRIM_25"
|
||||
ratio = 25
|
||||
reason = "TIME_STOP_APPROACHING"
|
||||
price = close_protect_limit
|
||||
price_source = "TIME_STOP_APPROACHING_CLOSE"
|
||||
price_basis = "ATR_PROTECT_LIMIT"
|
||||
execution_window = "CLOSE_REVIEW_OR_NEXT_OPEN"
|
||||
order_type = "LIMIT_SELL"
|
||||
|
||||
validation = "NO_SELL_ACTION"
|
||||
if action != "HOLD":
|
||||
validation = "SIGNAL_CONFIRMED" if isinstance(price, (int, float)) and float(price) > 0 else "NO_SELL_PRICE"
|
||||
|
||||
return {
|
||||
"action": action,
|
||||
"ratio_pct": ratio,
|
||||
"limit_price": price,
|
||||
"price_source": price_source,
|
||||
"price_basis": price_basis,
|
||||
"execution_window": execution_window,
|
||||
"order_type": order_type,
|
||||
"reason": reason,
|
||||
"validation": validation,
|
||||
"cash_preserve_style": "",
|
||||
"cash_preserve_ratio": 0,
|
||||
"cash_preserve_reason": [],
|
||||
}
|
||||
|
||||
|
||||
def compute_final_decision(ctx: dict[str, Any]) -> dict[str, Any]:
|
||||
sell_action = str(ctx.get("sellAction") or "HOLD")
|
||||
sell_validation = str(ctx.get("sellValidation") or "")
|
||||
allowed_action = str(ctx.get("allowedAction") or "")
|
||||
timing_action = str(ctx.get("timingAction") or "")
|
||||
timing_entry = ctx.get("timingScoreEntry")
|
||||
timing_exit = ctx.get("timingScoreExit")
|
||||
ss001_total = ctx.get("ss001Total")
|
||||
flow_credit = ctx.get("flowCredit")
|
||||
leader_total = ctx.get("leaderTotal")
|
||||
rw_partial = ctx.get("rwPartial")
|
||||
profit_pct = ctx.get("profitPct")
|
||||
days_to_time_stop = ctx.get("daysToTimeStop")
|
||||
weight_pct = ctx.get("weightPct")
|
||||
ac_gate = str(ctx.get("acGate") or "")
|
||||
liquidity_status = str(ctx.get("liquidityStatus") or "")
|
||||
spread_status = str(ctx.get("spreadStatus") or "")
|
||||
dart_risk = bool(ctx.get("dartRisk"))
|
||||
missing_fields = str(ctx.get("missingFields") or "")
|
||||
|
||||
final_action = "HOLD"
|
||||
action_priority = 99
|
||||
decision_source = "RULE_ENGINE"
|
||||
|
||||
if sell_action != "HOLD" and sell_validation == "SIGNAL_CONFIRMED":
|
||||
final_action = "SELL_READY"
|
||||
action_priority = 10
|
||||
elif allowed_action == "EXIT_SIGNAL" or timing_action == "STOP_OR_TIME_EXIT_READY":
|
||||
final_action = "EXIT_SIGNAL"
|
||||
action_priority = 28
|
||||
elif allowed_action == "REVIEW_EXIT" or timing_action == "EXIT_REVIEW":
|
||||
final_action = "EXIT_REVIEW"
|
||||
action_priority = 32
|
||||
elif timing_action == "NO_BUY_OVERHEATED" and not dart_risk:
|
||||
final_action = "NO_BUY_OVERHEATED"
|
||||
action_priority = 50
|
||||
elif allowed_action == "BUY_STAGE1_READY" or timing_action == "BUY_STAGE1_READY":
|
||||
final_action = "BUY_STAGE1_READY"
|
||||
action_priority = 60
|
||||
elif allowed_action == "BUY_BREAKOUT_PILOT_ONLY" or timing_action == "BUY_BREAKOUT_PILOT_ONLY":
|
||||
final_action = "BUY_BREAKOUT_PILOT_ONLY"
|
||||
action_priority = 70
|
||||
elif allowed_action == "BUY_PULLBACK_WAIT" or timing_action == "BUY_PULLBACK_WAIT":
|
||||
final_action = "BUY_PULLBACK_WAIT"
|
||||
action_priority = 80
|
||||
elif allowed_action == "WATCH_CANDIDATE":
|
||||
final_action = "WATCH_TIMING_SETUP"
|
||||
action_priority = 90
|
||||
|
||||
if missing_fields:
|
||||
decision_source = "RULE_ENGINE_WITH_MISSING_DATA"
|
||||
|
||||
def _finite(value: Any) -> bool:
|
||||
return isinstance(value, (int, float)) and math.isfinite(float(value))
|
||||
|
||||
time_stop_urgency = max(0, 20 - min(20, float(days_to_time_stop) * 3)) if _finite(days_to_time_stop) and days_to_time_stop >= 0 else 0
|
||||
overweight_penalty = 15 if _finite(weight_pct) and weight_pct > 7 else 0
|
||||
overheat_penalty = 30 if ac_gate == "BLOCK" else 10 if ac_gate == "CAUTION" else 0
|
||||
liquidity_penalty = 15 if liquidity_status in {"LOW", "DATA_MISSING"} or spread_status in {"BLOCK", "WIDE", "QUOTE_NO_MATCH"} else 0
|
||||
|
||||
if action_priority <= 40:
|
||||
priority_score = (float(timing_exit) if _finite(timing_exit) else 0) * 0.35 + (float(rw_partial) if _finite(rw_partial) else 0) * 15 + max(0, float(profit_pct) if _finite(profit_pct) else 0) * 0.30 + time_stop_urgency + overweight_penalty
|
||||
elif 50 <= action_priority <= 80:
|
||||
priority_score = (float(timing_entry) if _finite(timing_entry) else 0) * 0.35 + (float(ss001_total) if _finite(ss001_total) else 0) * 0.30 + (float(flow_credit) if _finite(flow_credit) else 0) * 20 + (float(leader_total) if _finite(leader_total) else 0) * 5 - overheat_penalty - liquidity_penalty
|
||||
else:
|
||||
priority_score = (float(timing_entry) if _finite(timing_entry) else 0) * 0.20 + (float(timing_exit) if _finite(timing_exit) else 0) * 0.20 + (float(flow_credit) if _finite(flow_credit) else 0) * 10
|
||||
|
||||
return {
|
||||
"final_action": final_action,
|
||||
"action_priority": action_priority,
|
||||
"priority_score": float(max(0, priority_score)),
|
||||
"decision_source": decision_source,
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"""generate_models_from_schema.py — schema model generation
|
||||
|
||||
Mirrors schemas/generated/*.schema.json into src/quant_engine/models/generated
|
||||
as lightweight Python model descriptors.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCHEMA_DIR = ROOT / "schemas" / "generated"
|
||||
OUT_DIR = ROOT / "src" / "quant_engine" / "models" / "generated"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
obj = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
return obj if isinstance(obj, dict) else {}
|
||||
|
||||
|
||||
def to_module_name(path: Path) -> str:
|
||||
return path.stem.replace(".", "_").lower()
|
||||
|
||||
|
||||
def render_module(schema_path: Path, schema: dict[str, Any]) -> str:
|
||||
title = str(schema.get("title") or schema_path.stem)
|
||||
schema_id = str(schema.get("$id") or f"schema://{title}")
|
||||
props = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
|
||||
required = schema.get("required") if isinstance(schema.get("required"), list) else []
|
||||
prop_names = list(props.keys())
|
||||
return (
|
||||
'"""Auto-generated schema model descriptor."""\n'
|
||||
"from __future__ import annotations\n\n"
|
||||
"from dataclasses import dataclass\n"
|
||||
"import json\n"
|
||||
"from pathlib import Path\n"
|
||||
"from typing import Any\n\n"
|
||||
f"SCHEMA_TITLE = {title!r}\n"
|
||||
f"SCHEMA_ID = {schema_id!r}\n"
|
||||
f"SCHEMA_PATH = {str(schema_path.relative_to(ROOT)).replace('\\', '/')!r}\n"
|
||||
f"SCHEMA_PROPERTIES = {prop_names!r}\n"
|
||||
f"SCHEMA_REQUIRED = {required!r}\n\n"
|
||||
"@dataclass(frozen=True)\n"
|
||||
"class SchemaModel:\n"
|
||||
" title: str\n"
|
||||
" schema_id: str\n"
|
||||
" path: str\n"
|
||||
" properties: list[str]\n"
|
||||
" required: list[str]\n\n"
|
||||
"def load_schema() -> dict[str, Any]:\n"
|
||||
" return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))\n\n"
|
||||
"def describe() -> SchemaModel:\n"
|
||||
" return SchemaModel(\n"
|
||||
" title=SCHEMA_TITLE,\n"
|
||||
" schema_id=SCHEMA_ID,\n"
|
||||
" path=SCHEMA_PATH,\n"
|
||||
" properties=list(SCHEMA_PROPERTIES),\n"
|
||||
" required=list(SCHEMA_REQUIRED),\n"
|
||||
" )\n"
|
||||
)
|
||||
|
||||
|
||||
def write_text(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--schemas", default=str(SCHEMA_DIR))
|
||||
parser.add_argument("--out", default=str(OUT_DIR))
|
||||
parser.add_argument("--report", default=str(ROOT / "Temp" / "schema_model_generation_v1.json"))
|
||||
args = parser.parse_args()
|
||||
|
||||
schema_dir = Path(args.schemas)
|
||||
out_dir = Path(args.out)
|
||||
|
||||
schema_files = sorted(schema_dir.glob("*.schema.json"))
|
||||
modules = []
|
||||
for schema_file in schema_files:
|
||||
schema = load_json(schema_file)
|
||||
module_name = to_module_name(schema_file)
|
||||
modules.append(module_name)
|
||||
write_text(out_dir / f"{module_name}.py", render_module(schema_file, schema))
|
||||
write_text(out_dir / f"{module_name}.schema.json", json.dumps(schema, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
write_text(out_dir / "__init__.py", '"""Auto-generated schema model package."""\n')
|
||||
write_text(
|
||||
out_dir.parent / "__init__.py",
|
||||
'"""Auto-generated quant_engine.models package."""\n',
|
||||
)
|
||||
write_text(
|
||||
out_dir.parent.parent / "__init__.py",
|
||||
'"""Canonical quant_engine package."""\n',
|
||||
)
|
||||
write_text(
|
||||
out_dir.parent.parent.parent / "__init__.py",
|
||||
'"""Canonical src package."""\n',
|
||||
)
|
||||
|
||||
report = {
|
||||
"status": "OK",
|
||||
"schema_count": len(schema_files),
|
||||
"generated_module_count": len(modules),
|
||||
"package_root": "src/quant_engine/models/generated",
|
||||
"report_path": str(Path(args.report).relative_to(ROOT)),
|
||||
}
|
||||
write_text(Path(args.report), json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,234 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import datetime as dt
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import openpyxl
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_XLSX = ROOT / "GatherTradingData.xlsx"
|
||||
|
||||
OUTPUT_HEADERS = [
|
||||
"ETF_Ticker",
|
||||
"ETF_Name",
|
||||
"Close",
|
||||
"NAV",
|
||||
"iNAV",
|
||||
"Premium_Discount_Pct",
|
||||
"Tracking_Error",
|
||||
"AUM",
|
||||
"Source_Date",
|
||||
"Source",
|
||||
"Enabled",
|
||||
"Note",
|
||||
]
|
||||
|
||||
COLUMN_ALIASES = {
|
||||
"ticker": ["ETF_Ticker", "종목코드", "단축코드", "표준코드", "code", "ticker"],
|
||||
"name": ["ETF_Name", "종목명", "한글종목명", "Name", "name"],
|
||||
"close": ["Close", "종가", "현재가", "시장가격", "TDD_CLSPRC", "close"],
|
||||
"nav": ["NAV", "순자산가치", "기준가격", "기준가", "NAV(원)", "nav"],
|
||||
"inav": ["iNAV", "추정순자산가치", "실시간기준가", "iNAV(원)", "inav"],
|
||||
"premium_discount_pct": ["Premium_Discount_Pct", "괴리율", "괴리율(%)", "가격괴리율", "premium_discount_pct"],
|
||||
"tracking_error": ["Tracking_Error", "추적오차율", "추적오차", "추적오차율(%)", "tracking_error"],
|
||||
"aum": ["AUM", "순자산총액", "순자산총액(원)", "상장좌수", "aum"],
|
||||
"source_date": ["Source_Date", "기준일", "일자", "거래일자", "Date", "date"],
|
||||
}
|
||||
|
||||
|
||||
def normalize_header(value: Any) -> str:
|
||||
return re.sub(r"\s+", "", str(value or "").strip()).lower()
|
||||
|
||||
|
||||
def normalize_ticker(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
if text.endswith(".0"):
|
||||
text = text[:-2]
|
||||
text = re.sub(r"[^0-9A-Za-z]", "", text)
|
||||
if text.isdigit():
|
||||
return text.zfill(6)
|
||||
if re.fullmatch(r"[0-9A-Za-z]{1,6}", text):
|
||||
return text.zfill(6)
|
||||
return text
|
||||
|
||||
|
||||
def parse_number(value: Any) -> float | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
return float(value)
|
||||
text = str(value).strip()
|
||||
if not text or text in {"-", "N/A", "nan"}:
|
||||
return None
|
||||
text = text.replace(",", "").replace("%", "")
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def parse_date(value: Any) -> str:
|
||||
if value in (None, ""):
|
||||
return ""
|
||||
if isinstance(value, (dt.datetime, dt.date)):
|
||||
return value.strftime("%Y-%m-%d")
|
||||
text = str(value).strip()
|
||||
match = re.search(r"(\d{4})[./-]?(\d{1,2})[./-]?(\d{1,2})", text)
|
||||
if not match:
|
||||
return ""
|
||||
y, m, d = match.groups()
|
||||
return f"{y}-{int(m):02d}-{int(d):02d}"
|
||||
|
||||
|
||||
def read_source_table(path: Path) -> list[dict[str, Any]]:
|
||||
if path.suffix.lower() in {".xlsx", ".xlsm"}:
|
||||
wb = openpyxl.load_workbook(path, data_only=True, read_only=True)
|
||||
ws = wb[wb.sheetnames[0]]
|
||||
rows = list(ws.iter_rows(values_only=True))
|
||||
header_row_idx = 0
|
||||
best_score = -1
|
||||
alias_tokens = {normalize_header(a) for aliases in COLUMN_ALIASES.values() for a in aliases}
|
||||
for i, row in enumerate(rows[:20]):
|
||||
score = sum(1 for cell in row if normalize_header(cell) in alias_tokens)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
header_row_idx = i
|
||||
headers = [str(v or "").strip() for v in rows[header_row_idx]]
|
||||
return [
|
||||
dict(zip(headers, row))
|
||||
for row in rows[header_row_idx + 1 :]
|
||||
if row and any(v not in (None, "") for v in row)
|
||||
]
|
||||
|
||||
encoding_candidates = ["utf-8-sig", "cp949", "euc-kr"]
|
||||
last_error: Exception | None = None
|
||||
for encoding in encoding_candidates:
|
||||
try:
|
||||
with path.open("r", encoding=encoding, newline="") as f:
|
||||
sample = f.read(4096)
|
||||
f.seek(0)
|
||||
dialect = csv.Sniffer().sniff(sample, delimiters=",\t;")
|
||||
return list(csv.DictReader(f, dialect=dialect))
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
raise RuntimeError(f"failed to read source file {path}: {last_error}")
|
||||
|
||||
|
||||
def resolve_columns(rows: list[dict[str, Any]]) -> dict[str, str]:
|
||||
if not rows:
|
||||
return {}
|
||||
source_headers = list(rows[0].keys())
|
||||
normalized = {normalize_header(h): h for h in source_headers}
|
||||
resolved: dict[str, str] = {}
|
||||
for field, aliases in COLUMN_ALIASES.items():
|
||||
for alias in aliases:
|
||||
key = normalize_header(alias)
|
||||
if key in normalized:
|
||||
resolved[field] = normalized[key]
|
||||
break
|
||||
return resolved
|
||||
|
||||
|
||||
def existing_etfs(wb: openpyxl.Workbook) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
if "etf_raw" in wb.sheetnames:
|
||||
ws = wb["etf_raw"]
|
||||
headers = [ws.cell(2, c).value for c in range(1, ws.max_column + 1)]
|
||||
idx = {h: i + 1 for i, h in enumerate(headers) if h}
|
||||
if "ETF_Ticker" in idx:
|
||||
for r in range(3, ws.max_row + 1):
|
||||
ticker = normalize_ticker(ws.cell(r, idx["ETF_Ticker"]).value)
|
||||
if ticker:
|
||||
result[ticker] = str(ws.cell(r, idx.get("ETF_Name", idx["ETF_Ticker"])).value or "")
|
||||
return result
|
||||
|
||||
|
||||
def update_workbook(workbook_path: Path, source_path: Path, enable: bool) -> tuple[int, int]:
|
||||
rows = read_source_table(source_path)
|
||||
columns = resolve_columns(rows)
|
||||
if "ticker" not in columns:
|
||||
raise RuntimeError(f"source file has no ticker/code column. resolved={columns}")
|
||||
|
||||
wb = openpyxl.load_workbook(workbook_path)
|
||||
targets = existing_etfs(wb)
|
||||
if "etf_nav_manual" in wb.sheetnames:
|
||||
del wb["etf_nav_manual"]
|
||||
insert_at = wb.sheetnames.index("etf_raw") + 1 if "etf_raw" in wb.sheetnames else 1
|
||||
ws = wb.create_sheet("etf_nav_manual", insert_at)
|
||||
ws.append([f"updated: imported from {source_path.name}"])
|
||||
ws.append(OUTPUT_HEADERS)
|
||||
|
||||
imported = 0
|
||||
matched = 0
|
||||
seen: set[str] = set()
|
||||
for row in rows:
|
||||
ticker = normalize_ticker(row.get(columns["ticker"]))
|
||||
if not ticker or ticker in seen:
|
||||
continue
|
||||
seen.add(ticker)
|
||||
name = str(row.get(columns.get("name", ""), "") or targets.get(ticker, "")).strip()
|
||||
close = parse_number(row.get(columns.get("close", "")))
|
||||
nav = parse_number(row.get(columns.get("nav", "")))
|
||||
inav = parse_number(row.get(columns.get("inav", "")))
|
||||
premium = parse_number(row.get(columns.get("premium_discount_pct", "")))
|
||||
if premium is None:
|
||||
basis_nav = nav if nav and nav > 0 else inav
|
||||
if close is not None and basis_nav and basis_nav > 0:
|
||||
premium = ((close / basis_nav) - 1) * 100
|
||||
tracking_error = parse_number(row.get(columns.get("tracking_error", "")))
|
||||
aum = parse_number(row.get(columns.get("aum", "")))
|
||||
source_date = parse_date(row.get(columns.get("source_date", "")))
|
||||
is_match = not targets or ticker in targets
|
||||
if is_match:
|
||||
matched += 1
|
||||
row_enable = "Y" if enable and is_match and (nav is not None or inav is not None) else "N"
|
||||
ws.append([
|
||||
ticker,
|
||||
name,
|
||||
close,
|
||||
nav,
|
||||
inav,
|
||||
premium,
|
||||
tracking_error,
|
||||
aum,
|
||||
source_date,
|
||||
f"import:{source_path.name}",
|
||||
row_enable,
|
||||
"matched_etf_raw" if is_match else "not_in_etf_raw_review_before_enable",
|
||||
])
|
||||
imported += 1
|
||||
|
||||
for row in ws.iter_rows(min_row=1, max_row=ws.max_row):
|
||||
row[0].number_format = "@"
|
||||
for cell in ws[2]:
|
||||
cell.font = openpyxl.styles.Font(bold=True, color="FFFFFF")
|
||||
cell.fill = openpyxl.styles.PatternFill("solid", fgColor="7030A0")
|
||||
ws.freeze_panes = "A3"
|
||||
ws.auto_filter.ref = f"A2:L{ws.max_row}"
|
||||
widths = [14, 34, 14, 14, 14, 20, 16, 16, 16, 28, 10, 42]
|
||||
for i, width in enumerate(widths, 1):
|
||||
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = width
|
||||
|
||||
wb.save(workbook_path)
|
||||
return imported, matched
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Import official ETF NAV/iNAV data into etf_nav_manual sheet.")
|
||||
parser.add_argument("source", type=Path, help="KRX/KIND/issuer CSV or XLSX export")
|
||||
parser.add_argument("--workbook", type=Path, default=DEFAULT_XLSX)
|
||||
parser.add_argument("--enable", action="store_true", help="Set Enabled=Y for matched rows with NAV or iNAV")
|
||||
args = parser.parse_args()
|
||||
|
||||
imported, matched = update_workbook(args.workbook, args.source, args.enable)
|
||||
print(f"ETF NAV IMPORT OK: imported={imported} matched_etf_raw={matched} workbook={args.workbook.name}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
lib_trading_calendar.py
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
KRX 거래일 기반 데이터 신선도 판정 모듈 (결정론)
|
||||
|
||||
배경: 한국 주식시장은 주말·공휴일 휴장한다. 금요일 종가 데이터는 토·일 내내
|
||||
변하지 않으므로, 단순 "24시간 경과" SLA는 토요일 오후만 돼도 거짓 신선도 위반을
|
||||
일으킨다. 신선도는 "캡처한 종가가 여전히 최신 종가인가"로 판정해야 한다.
|
||||
|
||||
핵심 규칙:
|
||||
데이터가 STALE인 시점 = 캡처 시각 이후 도래하는 첫 거래일 개장(09:00 KST).
|
||||
→ 금요일 15:35 캡처 → 다음 개장 = 월요일 09:00. 그 전까지 FRESH(페널티 0).
|
||||
|
||||
KST(UTC+9), KRX 정규장: 09:00 개장 / 15:30 마감.
|
||||
공휴일: spec/krx_holidays.yaml 에서 로드. 없으면 주말만 비거래일.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone, date, time
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HOLIDAYS_PATH = ROOT / "spec" / "krx_holidays.yaml"
|
||||
|
||||
KST = timezone(timedelta(hours=9))
|
||||
MARKET_OPEN_HOUR = 9 # 09:00 KST 개장
|
||||
MARKET_OPEN_MINUTE = 0
|
||||
MARKET_CLOSE_HOUR = 15 # 15:30 KST 마감
|
||||
MARKET_CLOSE_MINUTE = 30
|
||||
|
||||
|
||||
def _load_holidays() -> set[str]:
|
||||
"""공휴일 YYYY-MM-DD 문자열 집합. 파일 없으면 빈 집합(주말만 적용)."""
|
||||
if not HOLIDAYS_PATH.exists():
|
||||
return set()
|
||||
try:
|
||||
data = yaml.safe_load(HOLIDAYS_PATH.read_text(encoding="utf-8")) or {}
|
||||
hols = data.get("krx_market_holidays") or data.get("holidays") or []
|
||||
result: set[str] = set()
|
||||
for h in hols:
|
||||
d = str(h.get("date") if isinstance(h, dict) else h or "").strip()
|
||||
if d:
|
||||
result.add(d[:10])
|
||||
return result
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
_HOLIDAYS_CACHE: set[str] | None = None
|
||||
|
||||
|
||||
def _holidays() -> set[str]:
|
||||
global _HOLIDAYS_CACHE
|
||||
if _HOLIDAYS_CACHE is None:
|
||||
_HOLIDAYS_CACHE = _load_holidays()
|
||||
return _HOLIDAYS_CACHE
|
||||
|
||||
|
||||
def is_trading_day(d: date) -> bool:
|
||||
"""거래일 여부 — 주말(토5/일6) 및 공휴일 제외."""
|
||||
if d.weekday() >= 5: # 5=토, 6=일
|
||||
return False
|
||||
if d.isoformat() in _holidays():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def next_trading_day(d: date) -> date:
|
||||
"""d 다음(d 제외) 첫 거래일."""
|
||||
nxt = d + timedelta(days=1)
|
||||
for _ in range(30): # 연속 휴장 한계 방어
|
||||
if is_trading_day(nxt):
|
||||
return nxt
|
||||
nxt += timedelta(days=1)
|
||||
return nxt
|
||||
|
||||
|
||||
def market_open_dt(d: date) -> datetime:
|
||||
"""거래일 d 의 개장 시각(KST aware)."""
|
||||
return datetime.combine(d, time(MARKET_OPEN_HOUR, MARKET_OPEN_MINUTE), tzinfo=KST)
|
||||
|
||||
|
||||
def next_market_open_after(dt_utc: datetime) -> datetime:
|
||||
"""주어진 시각(UTC aware) 이후 도래하는 첫 거래일 개장 시각(KST aware).
|
||||
|
||||
예: 금요일 15:35 KST → 월요일 09:00 KST (토·일 건너뜀)
|
||||
금요일 08:00 KST → 금요일 09:00 KST
|
||||
월요일 10:00 KST → 화요일 09:00 KST
|
||||
"""
|
||||
dt_kst = dt_utc.astimezone(KST)
|
||||
cur_date = dt_kst.date()
|
||||
|
||||
# 오늘이 거래일이고 아직 개장(09:00) 전이면 → 오늘 개장
|
||||
if is_trading_day(cur_date):
|
||||
today_open = market_open_dt(cur_date)
|
||||
if dt_kst < today_open:
|
||||
return today_open
|
||||
|
||||
nd = next_trading_day(cur_date)
|
||||
return market_open_dt(nd)
|
||||
|
||||
|
||||
def is_data_stale(captured_at_iso: str, now_utc: datetime | None = None) -> dict:
|
||||
"""거래일 기반 데이터 신선도 판정.
|
||||
|
||||
Returns dict: stale(bool), captured_at_kst, stale_deadline_kst, now_kst,
|
||||
hours_until_stale(양수=FRESH, 음수=STALE), reason.
|
||||
"""
|
||||
if now_utc is None:
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
dt = datetime.fromisoformat(captured_at_iso.replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
except Exception:
|
||||
return {
|
||||
"stale": True,
|
||||
"captured_at_kst": None,
|
||||
"stale_deadline_kst": None,
|
||||
"now_kst": now_utc.astimezone(KST).isoformat(),
|
||||
"hours_until_stale": None,
|
||||
"reason": "INVALID_CAPTURED_AT",
|
||||
}
|
||||
|
||||
deadline = next_market_open_after(dt)
|
||||
now_kst = now_utc.astimezone(KST)
|
||||
stale = now_kst >= deadline
|
||||
hours_until = (deadline - now_kst).total_seconds() / 3600.0
|
||||
|
||||
return {
|
||||
"stale": stale,
|
||||
"captured_at_kst": dt.astimezone(KST).isoformat(),
|
||||
"stale_deadline_kst": deadline.isoformat(),
|
||||
"now_kst": now_kst.isoformat(),
|
||||
"hours_until_stale": round(hours_until, 2),
|
||||
"reason": "STALE_NEW_SESSION_OPENED" if stale else "FRESH_WITHIN_TRADING_SESSION",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
# 셀프 테스트 (KST 기준: 금 15:35 = UTC 06:35)
|
||||
cases = [
|
||||
("2026-05-29T06:35:00+00:00", "2026-05-30T05:00:00+00:00", "금15:35캡처 → 토14:00 KST: FRESH 기대"),
|
||||
("2026-05-29T06:35:00+00:00", "2026-05-31T23:30:00+00:00", "금15:35캡처 → 월08:30 KST: FRESH 기대"),
|
||||
("2026-05-29T06:35:00+00:00", "2026-06-01T00:30:00+00:00", "금15:35캡처 → 월09:30 KST: STALE 기대"),
|
||||
]
|
||||
for cap, now, desc in cases:
|
||||
r = is_data_stale(cap, datetime.fromisoformat(now))
|
||||
print(f"{desc}\n stale={r['stale']} deadline={r['stale_deadline_kst']} hours_until={r['hours_until_stale']}\n")
|
||||
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
measure_harness_coverage.py
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
하네스 커버리지 측정기
|
||||
|
||||
"YAML 스펙을 작성해도 GAS가 실제로 계산하지 않으면 LLM이 매번 다른 숫자를 만든다."
|
||||
이 도구는 현재 harness_context에서 GAS가 실제 채운 수치 필드 vs
|
||||
LLM이 추정해야 하는 공백 필드를 정량 측정한다.
|
||||
|
||||
출력:
|
||||
- 전체 커버리지 % (GAS 산출 / 전체 필수 필드)
|
||||
- 공식별 커버리지 표
|
||||
- LLM 자유도 점수 (낮을수록 결정론적)
|
||||
- 재현성 위험 필드 목록 (LLM이 계산해야 하는 필드 = 랜덤성 원천)
|
||||
|
||||
사용법:
|
||||
python tools/measure_harness_coverage.py [GatherTradingData.json]
|
||||
python tools/measure_harness_coverage.py [GatherTradingData.json] --strict-100
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# ── 공식별 필수 출력 필드 정의 ──────────────────────────────────────────────
|
||||
# (field_name, description, data_type)
|
||||
FORMULA_OUTPUT_FIELDS: dict[str, list[tuple[str, str, str]]] = {
|
||||
# ── STAGE 0 ──────────────────────────────────────────────────────────────
|
||||
"HARNESS_DATA_FRESHNESS_GATE_V1": [
|
||||
("data_freshness_status", "데이터 신선도 상태", "enum"),
|
||||
],
|
||||
"INTRADAY_ACTION_MATRIX_V1": [
|
||||
("intraday_scope", "장중/장전 허용 액션 범위", "enum"),
|
||||
("intraday_lock", "장중 잠금 여부", "bool"),
|
||||
],
|
||||
# ── STAGE 1 ──────────────────────────────────────────────────────────────
|
||||
"CASH_RATIOS_V1": [
|
||||
("settlement_cash_d2_krw", "D+2 정산현금(원)", "numeric"),
|
||||
("settlement_cash_pct", "D+2 현금 비율(%)", "numeric"),
|
||||
("cash_floor_min_pct", "최소 현금 바닥(%)", "numeric"),
|
||||
("cash_shortfall_min_krw", "현금 부족분(원)", "numeric"),
|
||||
],
|
||||
"TOTAL_HEAT_V1": [
|
||||
("total_heat_pct", "포트폴리오 총 Heat(%)", "numeric"),
|
||||
("heat_gate_status", "Heat 게이트 상태", "enum"),
|
||||
],
|
||||
# ── STAGE 2 ──────────────────────────────────────────────────────────────
|
||||
"PROFIT_LOCK_RATCHET_V1": [
|
||||
("profit_lock_stage", "수익 잠금 단계", "enum"),
|
||||
("auto_trailing_stop", "ATR 기반 자동 트레일링", "numeric"),
|
||||
],
|
||||
"PROFIT_RATCHET_TIERED_V2": [
|
||||
("auto_trailing_stop_v2", "3RD — APEX_SUPER 래칫", "numeric"),
|
||||
("ratchet_stage_v2", "래칫 단계 v2", "enum"),
|
||||
],
|
||||
# ── STAGE 3 ──────────────────────────────────────────────────────────────
|
||||
"FLOW_ACCELERATION_V1": [
|
||||
("flow_acceleration_status", "수급 에너지 소진 상태", "enum"),
|
||||
],
|
||||
"DISTRIBUTION_SELL_DETECTOR_V1": [
|
||||
("distribution_sell_detector_status", "설거지 감지 상태 (6신호)", "enum"),
|
||||
("signals_count", "트리거된 신호 수", "numeric"),
|
||||
],
|
||||
# ── STAGE 4 ──────────────────────────────────────────────────────────────
|
||||
"BREAKOUT_QUALITY_GATE_V2": [
|
||||
("breakout_quality_score", "돌파 품질 점수", "numeric"),
|
||||
],
|
||||
"ANTI_CHASING_VELOCITY_V1": [
|
||||
("anti_chasing_verdict", "뒷박 추격 차단 판정", "enum"),
|
||||
("anti_chasing_velocity_status", "속도 차단 상태", "enum"),
|
||||
],
|
||||
"PULLBACK_ENTRY_TRIGGER_V1": [
|
||||
("pullback_entry_verdict", "눌림목 진입 판정", "enum"),
|
||||
("pullback_entry_trigger_price", "허용 진입 기준가(원)", "numeric"),
|
||||
],
|
||||
# ── STAGE 5 ──────────────────────────────────────────────────────────────
|
||||
"CASH_RECOVERY_OPTIMIZER_V1": [
|
||||
("cash_recovery_plan_json", "현금회복 최적 매도조합 JSON", "json"),
|
||||
],
|
||||
"SELL_WATERFALL_ENGINE_V1": [
|
||||
("waterfall_plan_json", "폭포수 매도 계획 JSON", "json"),
|
||||
],
|
||||
"SELL_EXECUTION_TIMING_V1": [
|
||||
("sell_timing_verdict", "매도 실행 타이밍 판정", "enum"),
|
||||
("sell_execution_window", "실행 허용 시간대", "enum"),
|
||||
],
|
||||
"SELL_VALUE_PRESERVATION_TIERED_V2": [
|
||||
("preservation_verdict", "주식가치 보호 매도 판정", "enum"),
|
||||
],
|
||||
# ── STAGE 6 ──────────────────────────────────────────────────────────────
|
||||
"TICK_NORMALIZER_V1": [
|
||||
("tick_normalized_price", "호가 정규화 완료 표시", "bool"),
|
||||
],
|
||||
"SELL_PRICE_SANITY_V1": [
|
||||
("sell_price_sanity_status", "매도가 역전/비현실가 검증", "enum"),
|
||||
],
|
||||
# ── STAGE 7 ──────────────────────────────────────────────────────────────
|
||||
"BENCHMARK_RELATIVE_TIMESERIES_V1": [
|
||||
("brt_verdict", "BRT 상대강도 판정", "enum"),
|
||||
("brt_rs_slope", "RS 기울기", "numeric"),
|
||||
],
|
||||
"RS_VERDICT_V2": [
|
||||
("rs_verdict", "최종 RS 판정", "enum"),
|
||||
],
|
||||
# ── STAGE 8 ──────────────────────────────────────────────────────────────
|
||||
"SATELLITE_ALPHA_QUALITY_GATE_V1": [
|
||||
("saqg_verdict", "위성 품질 게이트", "enum"),
|
||||
],
|
||||
"SATELLITE_AGGREGATE_PNL_GATE_V1": [
|
||||
("sapg_verdict", "위성 합산 손익 게이트", "enum"),
|
||||
],
|
||||
# ── STAGE 9 ──────────────────────────────────────────────────────────────
|
||||
"LLM_SERVING_CONSTRAINT_V1": [
|
||||
("serving_constraint_check", "LLM 제약 검사 결과", "enum"),
|
||||
],
|
||||
"DETERMINISTIC_ROUTING_ENGINE_V1": [
|
||||
("routing_execution_log", "9단계 라우팅 실행 로그", "json"),
|
||||
],
|
||||
# ── MONTHLY BATCH ─────────────────────────────────────────────────────────
|
||||
"TRADE_QUALITY_SCORER_V1": [
|
||||
("trade_quality_json", "거래 품질 채점 결과 JSON", "json"),
|
||||
],
|
||||
"PATTERN_BLACKLIST_AUTO_V1": [
|
||||
("pattern_blacklist_status", "반복 패턴 블랙리스트 상태", "enum"),
|
||||
],
|
||||
# ── 기존 필수 필드 ─────────────────────────────────────────────────────────
|
||||
"POSITION_SIZE_V1": [
|
||||
("buy_power_krw", "매수 가용 현금(원)", "numeric"),
|
||||
("total_asset_krw", "총 자산(원)", "numeric"),
|
||||
],
|
||||
"prices_lock": [
|
||||
("prices_json", "가격 잠금 JSON (stop/tp/current)", "json"),
|
||||
],
|
||||
"quantities_lock": [
|
||||
("sell_quantities_json", "매도 수량 잠금 JSON", "json"),
|
||||
("buy_qty_inputs_json", "매수 수량 잠금 JSON", "json"),
|
||||
("order_blueprint_json", "HTS 주문 청사진 JSON", "json"),
|
||||
],
|
||||
}
|
||||
|
||||
SEP = "=" * 70
|
||||
SEP2 = "-" * 70
|
||||
|
||||
|
||||
def load_harness_context(json_path: Path) -> dict:
|
||||
raw = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
hc = None
|
||||
try:
|
||||
hc = raw["data"]["_harness_context"]
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
if hc is None:
|
||||
for key in ["_harness_context", "harness_context"]:
|
||||
if key in raw and isinstance(raw[key], dict):
|
||||
hc = raw[key]
|
||||
break
|
||||
if hc is None:
|
||||
print("[ERROR] harness_context를 찾을 수 없음")
|
||||
sys.exit(1)
|
||||
return hc
|
||||
|
||||
|
||||
def is_field_present(hc: dict, field: str) -> bool:
|
||||
val = hc.get(field)
|
||||
if val is None:
|
||||
return False
|
||||
if isinstance(val, str) and val.strip() == "":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def field_is_numeric(hc: dict, field: str) -> bool:
|
||||
val = hc.get(field)
|
||||
return isinstance(val, (int, float)) and not isinstance(val, bool)
|
||||
|
||||
|
||||
def compute_coverage(hc: dict) -> dict[str, object]:
|
||||
total_fields = 0
|
||||
covered_fields = 0
|
||||
missing_fields: list[tuple[str, str, str]] = []
|
||||
covered_list: list[tuple[str, str]] = []
|
||||
formula_results: list[dict[str, object]] = []
|
||||
|
||||
for formula_id, fields in FORMULA_OUTPUT_FIELDS.items():
|
||||
f_total = len(fields)
|
||||
f_covered = 0
|
||||
f_missing: list[str] = []
|
||||
|
||||
for field_name, _description, dtype in fields:
|
||||
total_fields += 1
|
||||
if is_field_present(hc, field_name):
|
||||
covered_fields += 1
|
||||
f_covered += 1
|
||||
covered_list.append((formula_id, field_name))
|
||||
else:
|
||||
f_missing.append(field_name)
|
||||
missing_fields.append((formula_id, field_name, dtype))
|
||||
|
||||
pct = f_covered / f_total * 100 if f_total > 0 else 0
|
||||
formula_results.append({
|
||||
"formula_id": formula_id,
|
||||
"total": f_total,
|
||||
"covered": f_covered,
|
||||
"pct": pct,
|
||||
"missing": f_missing,
|
||||
})
|
||||
|
||||
overall_pct = covered_fields / total_fields * 100 if total_fields > 0 else 0
|
||||
return {
|
||||
"total_fields": total_fields,
|
||||
"covered_fields": covered_fields,
|
||||
"overall_pct": overall_pct,
|
||||
"llm_freedom_score": 100 - overall_pct,
|
||||
"missing_fields": missing_fields,
|
||||
"covered_list": covered_list,
|
||||
"formula_results": formula_results,
|
||||
}
|
||||
|
||||
|
||||
def ensure_utf8_stdio() -> None:
|
||||
# Windows cp949 터미널 호환
|
||||
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
||||
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1)
|
||||
if sys.stderr.encoding and sys.stderr.encoding.lower() not in ("utf-8", "utf8"):
|
||||
sys.stderr = open(sys.stderr.fileno(), mode="w", encoding="utf-8", buffering=1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ensure_utf8_stdio()
|
||||
strict_100 = "--strict-100" in sys.argv
|
||||
argv = [arg for arg in sys.argv[1:] if arg != "--strict-100"]
|
||||
json_path = Path(argv[0]) if argv else ROOT / "GatherTradingData.json"
|
||||
if not json_path.exists():
|
||||
print(f"[ERROR] {json_path} not found")
|
||||
return 1
|
||||
|
||||
hc = load_harness_context(json_path)
|
||||
coverage = compute_coverage(hc)
|
||||
|
||||
print(SEP)
|
||||
print(" 하네스 커버리지 측정기 — Harness Coverage Report")
|
||||
print(f" 파일: {json_path.name}")
|
||||
print(f" harness_version: {hc.get('harness_version', '(missing)')}")
|
||||
print(f" computed_at: {hc.get('computed_at', '(missing)')}")
|
||||
print(SEP)
|
||||
|
||||
# ── 공식별 커버리지 표 ──────────────────────────────────────────────────
|
||||
print("\n[공식별 커버리지]")
|
||||
print(f" {'공식 ID':<45} {'커버':<6} {'전체':<6} {'%':<7} 상태")
|
||||
print(" " + "-" * 65)
|
||||
for r in coverage["formula_results"]:
|
||||
bar = "●" * r["covered"] + "○" * (r["total"] - r["covered"])
|
||||
status = "✔ FULL" if r["pct"] == 100 else ("△ PARTIAL" if r["pct"] > 0 else "✗ MISSING")
|
||||
print(f" {r['formula_id']:<45} {r['covered']:<6} {r['total']:<6} {r['pct']:>5.0f}% {status} {bar}")
|
||||
|
||||
# ── 전체 커버리지 요약 ──────────────────────────────────────────────────
|
||||
overall_pct = coverage["overall_pct"]
|
||||
llm_freedom_score = coverage["llm_freedom_score"] # 높을수록 LLM이 더 많이 추정
|
||||
|
||||
print()
|
||||
print(SEP)
|
||||
print(f" 전체 커버리지 : {coverage['covered_fields']}/{coverage['total_fields']} 필드 = {overall_pct:.1f}%")
|
||||
print(f" LLM 자유도 점수 : {llm_freedom_score:.1f}% ← 낮을수록 결정론적 (목표: 0%)")
|
||||
print(SEP)
|
||||
|
||||
if llm_freedom_score == 0:
|
||||
print("\n ✔ 완전 결정론적 — LLM이 임의 계산해야 할 필드 없음")
|
||||
else:
|
||||
# ── 재현성 위험 필드 목록 ─────────────────────────────────────────
|
||||
missing_fields = coverage["missing_fields"]
|
||||
print(f"\n[재현성 위험 필드 — GAS 미계산 = LLM 추정 = 랜덤성 원천] ({len(missing_fields)}개)")
|
||||
print(" 이 필드들은 LLM 호출마다 다른 값이 나올 수 있습니다.\n")
|
||||
print(f" {'공식 ID':<45} {'필드명':<40} 타입")
|
||||
print(" " + "-" * 95)
|
||||
for formula_id, field_name, dtype in missing_fields:
|
||||
print(f" {formula_id:<45} {field_name:<40} {dtype}")
|
||||
|
||||
# ── 수치 필드 실제 값 확인 (GAS 계산 완료된 필드) ──────────────────────
|
||||
covered_list = coverage["covered_list"]
|
||||
formula_results = coverage["formula_results"]
|
||||
print(f"\n[GAS 계산 완료 수치 필드] ({len(covered_list)}개)")
|
||||
numeric_present = [
|
||||
(fid, fn, hc[fn])
|
||||
for fid, fn in covered_list
|
||||
if field_is_numeric(hc, fn)
|
||||
]
|
||||
for fid, fn, val in numeric_present[:20]:
|
||||
print(f" {fn:<45} = {val:>15,.0f}" if isinstance(val, (int, float)) else f" {fn:<45} = {val}")
|
||||
|
||||
# ── GAS 구현 우선순위 권고 ──────────────────────────────────────────────
|
||||
print(f"\n[GAS 구현 우선순위 — 커버리지 0% 공식부터]")
|
||||
zero_coverage = [r for r in formula_results if r["pct"] == 0]
|
||||
for r in zero_coverage:
|
||||
print(f" !!! {r['formula_id']} — 출력 필드 {r['total']}개 전부 미계산")
|
||||
|
||||
print()
|
||||
threshold = 100.0 if strict_100 else 80.0
|
||||
return 0 if overall_pct >= threshold else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,352 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# ── 셀-레벨 커버리지: yaml expected_outputs → operational_report 셀 매핑 ──────
|
||||
# 각 formula의 expected_outputs 필드가 operational_report의 표 셀에 채워졌는지 측정.
|
||||
# _CELL_COVERAGE_STUBS: 채워진 것처럼 보이지만 실제 데이터 없는 일률 stub 값들
|
||||
_CELL_COVERAGE_STUBS = frozenset({
|
||||
"", "-", "n/a", "N/A", "데이터 누락", "DATA_MISSING", "중립", "NEUTRAL",
|
||||
"LOSING", "정상", "NORMAL", "MISSING", "WATCH_PENDING_SAMPLE",
|
||||
})
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> dict[str, Any]:
|
||||
payload = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def load_json_safe(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
v = json.loads(path.read_text(encoding="utf-8"))
|
||||
return v if isinstance(v, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def formula_registry() -> dict[str, Any]:
|
||||
"""formula_id → formula_dict (expected_outputs 포함)."""
|
||||
registry: dict[str, Any] = {}
|
||||
for p in (ROOT / "spec" / "13_formula_registry.yaml", ROOT / "spec" / "13b_harness_formulas.yaml"):
|
||||
y = load_yaml(p)
|
||||
fm = ((y.get("formula_registry") or {}).get("formulas")) or {}
|
||||
for k, v in fm.items():
|
||||
if isinstance(v, dict):
|
||||
registry[str(k)] = v
|
||||
return registry
|
||||
|
||||
|
||||
def formula_ids() -> list[str]:
|
||||
return sorted(formula_registry().keys())
|
||||
|
||||
|
||||
def read_texts(paths: list[Path]) -> str:
|
||||
chunks: list[str] = []
|
||||
for p in paths:
|
||||
if p.exists():
|
||||
chunks.append(p.read_text(encoding="utf-8", errors="ignore"))
|
||||
return "\n".join(chunks)
|
||||
|
||||
|
||||
def _extract_table_cells(markdown: str) -> set[str]:
|
||||
"""GFM 표에서 셀 값 목록을 추출 (헤더 + 데이터 행)."""
|
||||
cells: set[str] = set()
|
||||
for line in markdown.split("\n"):
|
||||
if "|" not in line:
|
||||
continue
|
||||
parts = [p.strip() for p in line.split("|")]
|
||||
for p in parts:
|
||||
if p and p != "---" and not re.match(r"^-+$", p):
|
||||
cells.add(p)
|
||||
return cells
|
||||
|
||||
|
||||
def _is_stub(value: str) -> bool:
|
||||
return value.strip() in _CELL_COVERAGE_STUBS or value.strip().startswith("-")
|
||||
|
||||
|
||||
def measure_cell_coverage(
|
||||
formula_reg: dict[str, Any],
|
||||
report_json: dict[str, Any],
|
||||
harness_ctx: dict[str, Any],
|
||||
temp_outputs: dict[str, dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
"""yaml expected_outputs → 4경로 커버리지.
|
||||
|
||||
출력 필드가 채워진 것으로 인정하는 4가지 경로:
|
||||
1. GAS harness_context에 output.field 키가 non-null 존재
|
||||
2. Phase-1 Temp JSON 파일에 expected_output 필드가 non-stub 존재
|
||||
3. operational_report 섹션 텍스트에 expected_output 이름이 column header로 존재
|
||||
4. operational_report 표 셀에 non-stub 값으로 필드명=값 패턴 존재
|
||||
"""
|
||||
# Collect all report text (markdown)
|
||||
all_section_text = ""
|
||||
for sec in report_json.get("sections") or []:
|
||||
all_section_text += " " + (sec.get("markdown") or "")
|
||||
|
||||
# Flatten all Temp output values for quick lookup
|
||||
temp_flat: dict[str, Any] = {}
|
||||
for _fname, tdata in temp_outputs.items():
|
||||
if isinstance(tdata, dict):
|
||||
# Flatten top-level scalars and row-level fields
|
||||
for k, v in tdata.items():
|
||||
if k not in ("rows", "steps", "selected_combo"):
|
||||
temp_flat[k] = v
|
||||
# Also include fields from first row of any rows list
|
||||
for listkey in ("rows", "steps", "selected_combo"):
|
||||
lst = tdata.get(listkey)
|
||||
if isinstance(lst, list) and lst and isinstance(lst[0], dict):
|
||||
for k, v in lst[0].items():
|
||||
temp_flat.setdefault(k, v)
|
||||
|
||||
required_outputs: list[dict[str, Any]] = []
|
||||
for fid, fdef in formula_reg.items():
|
||||
if not isinstance(fdef, dict):
|
||||
continue
|
||||
# orphan reconcile 공식은 GAS/보고서 셀 검사 제외 (Python harness 전용)
|
||||
if str(fdef.get("version", "")).endswith("_ORPHAN_RECONCILE"):
|
||||
continue
|
||||
exp = fdef.get("expected_outputs")
|
||||
if not isinstance(exp, list):
|
||||
continue
|
||||
out_field = (fdef.get("output") or {}).get("field") if isinstance(fdef.get("output"), dict) else None
|
||||
|
||||
# Path 1: GAS harness_context
|
||||
ctx_present = bool(out_field and harness_ctx.get(out_field) is not None)
|
||||
|
||||
for o in exp:
|
||||
field_name = str(o).strip() if isinstance(o, str) else str(o).strip()
|
||||
|
||||
# Path 2: Temp JSON outputs (Phase-1 Python tools)
|
||||
temp_val = temp_flat.get(field_name)
|
||||
temp_filled = temp_val is not None and str(temp_val).strip() not in _CELL_COVERAGE_STUBS
|
||||
|
||||
# Path 3: Column header in report
|
||||
in_report_header = bool(field_name and field_name in all_section_text)
|
||||
|
||||
# Path 4: Row-level cell value (non-stub)
|
||||
non_stub_value = False
|
||||
pat = re.search(
|
||||
rf"\b{re.escape(field_name)}\b[^|\n]*\|([^|\n]+)", all_section_text
|
||||
)
|
||||
if pat:
|
||||
val_candidate = pat.group(1).strip()
|
||||
non_stub_value = not _is_stub(val_candidate)
|
||||
|
||||
filled = ctx_present or temp_filled or (in_report_header and non_stub_value)
|
||||
|
||||
required_outputs.append({
|
||||
"formula_id": fid,
|
||||
"output_field": field_name,
|
||||
"ctx_present": ctx_present,
|
||||
"temp_filled": temp_filled,
|
||||
"in_report_header": in_report_header,
|
||||
"non_stub_value": non_stub_value,
|
||||
"filled": filled,
|
||||
})
|
||||
|
||||
total = len(required_outputs)
|
||||
filled_count = sum(1 for r in required_outputs if r["filled"])
|
||||
cell_coverage_pct = round(filled_count / total * 100, 2) if total > 0 else 0.0
|
||||
unfilled = [r for r in required_outputs if not r["filled"]]
|
||||
|
||||
return {
|
||||
"total_required_outputs": total,
|
||||
"filled_outputs": filled_count,
|
||||
"cell_coverage_pct": cell_coverage_pct,
|
||||
"unfilled_outputs": unfilled,
|
||||
"cell_gate": "PASS" if cell_coverage_pct >= 95.0 else ("CAUTION" if cell_coverage_pct >= 75.0 else "FAIL"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Measure YAML formula coverage in GS and PS governance layers.")
|
||||
parser.add_argument("--strict-100", action="store_true")
|
||||
parser.add_argument("--output-json", default=str(ROOT / "Temp" / "yaml_gs_ps_coverage.json"))
|
||||
parser.add_argument("--report-json", default=str(ROOT / "Temp" / "operational_report.json"))
|
||||
args = parser.parse_args()
|
||||
|
||||
reg = formula_registry()
|
||||
ids = sorted(reg.keys())
|
||||
_GS_CORE = [ROOT / "gas_data_feed.gs", ROOT / "gas_harness_rows.gs", ROOT / "gas_lib.gs", ROOT / "gas_data_collect.gs", ROOT / "gas_report.gs"]
|
||||
_GAS_ADAPTER_DIR = ROOT / "src" / "gas_adapter_parts"
|
||||
_gs_adapter_files = sorted(_GAS_ADAPTER_DIR.glob("*.gs")) if _GAS_ADAPTER_DIR.is_dir() else []
|
||||
_GS_ALPHA_WATCH = [ROOT / "gas_apex_alpha_watch.gs", ROOT / "gas_apex_runtime_core.gs"]
|
||||
gs_text = read_texts(_GS_CORE + _gs_adapter_files + _GS_ALPHA_WATCH)
|
||||
ps_text = read_texts([ROOT / "tools" / "run_engine_harness_gate.ps1", ROOT / "tools" / "run_yolo_full_cycle.ps1"])
|
||||
gate_py_text = read_texts([ROOT / "tools" / "validate_engine_harness_gate.py"])
|
||||
|
||||
gs_hit = [i for i in ids if i in gs_text]
|
||||
gs_miss = [i for i in ids if i not in gs_text]
|
||||
|
||||
# PS는 공식 직접 계산 계층이 아니라 실행 강제 계층.
|
||||
ps_required_hooks = [
|
||||
("run_engine_harness_gate.ps1", ROOT / "tools" / "run_engine_harness_gate.ps1"),
|
||||
("run_yolo_full_cycle.ps1", ROOT / "tools" / "run_yolo_full_cycle.ps1"),
|
||||
("validate_engine_harness_gate.py", ROOT / "tools" / "validate_engine_harness_gate.py"),
|
||||
]
|
||||
ps_hook_hit = [name for name, path in ps_required_hooks if path.exists()]
|
||||
ps_hook_miss = [name for name, path in ps_required_hooks if not path.exists()]
|
||||
|
||||
total = len(ids) if ids else 1
|
||||
gs_pct = round(len(gs_hit) / total * 100, 2)
|
||||
ps_pct = round(len(ps_hook_hit) / len(ps_required_hooks) * 100, 2)
|
||||
|
||||
# ── 셀-레벨 커버리지 측정 ──────────────────────────────────────────────────
|
||||
report_json_path = Path(args.report_json)
|
||||
if not report_json_path.is_absolute():
|
||||
report_json_path = ROOT / report_json_path
|
||||
report_json = load_json_safe(report_json_path)
|
||||
# harness context from GatherTradingData.json
|
||||
gtd = load_json_safe(ROOT / "GatherTradingData.json")
|
||||
hctx = (gtd.get("data") or {}).get("_harness_context") or {}
|
||||
# Phase-1/2/3 Temp outputs (Python tools)
|
||||
_TEMP = ROOT / "Temp"
|
||||
temp_outputs = {
|
||||
# Phase-1
|
||||
"ejce_view_renderer_v1": load_json_safe(_TEMP / "ejce_view_renderer_v1.json"),
|
||||
"smart_cash_recovery_v3": load_json_safe(_TEMP / "smart_cash_recovery_v3.json"),
|
||||
"ratchet_trailing_v1": load_json_safe(_TEMP / "ratchet_trailing_general_v1.json"),
|
||||
"value_preservation_v1": load_json_safe(_TEMP / "value_preservation_scorer_v1.json"),
|
||||
"routing_execution_log_v1": load_json_safe(_TEMP / "routing_execution_log_v1.json"),
|
||||
"blank_cell_audit_v1": load_json_safe(_TEMP / "blank_cell_audit_v1.json"),
|
||||
"formula_registry_sync_v1": load_json_safe(_TEMP / "formula_registry_sync_v1.json"),
|
||||
# Phase-2
|
||||
"fundamental_raw_v1": load_json_safe(_TEMP / "fundamental_raw_v1.json"),
|
||||
"fundamental_multifactor_v3": load_json_safe(_TEMP / "fundamental_multifactor_v3.json"),
|
||||
"horizon_classification_v1": load_json_safe(_TEMP / "horizon_classification_v1.json"),
|
||||
# Phase-2B
|
||||
"earnings_quality_signal_v1": load_json_safe(_TEMP / "earnings_quality_signal_v1.json"),
|
||||
"growth_rate_signal_v1": load_json_safe(_TEMP / "growth_rate_signal_v1.json"),
|
||||
"cashflow_quality_signal_v1": load_json_safe(_TEMP / "cashflow_quality_signal_v1.json"),
|
||||
"market_share_signal_v2": load_json_safe(_TEMP / "market_share_signal_v2.json"),
|
||||
# Phase-3
|
||||
"smart_money_flow_signal_v2": load_json_safe(_TEMP / "smart_money_flow_signal_v2.json"),
|
||||
"liquidity_flow_signal_v1": load_json_safe(_TEMP / "liquidity_flow_signal_v1.json"),
|
||||
"capital_style_allocation_v1": load_json_safe(_TEMP / "capital_style_allocation_v1.json"),
|
||||
"portfolio_alpha_confidence_per_ticker_v1": load_json_safe(_TEMP / "portfolio_alpha_confidence_per_ticker_v1.json"),
|
||||
# [Advanced Harness Architecture]
|
||||
"dynamic_value_preservation_sell_v6": load_json_safe(_TEMP / "dynamic_value_preservation_sell_v6.json"),
|
||||
"predictive_alpha_engine_v2": load_json_safe(_TEMP / "predictive_alpha_engine_v2.json"),
|
||||
"capital_style_time_stop_v1": load_json_safe(_TEMP / "capital_style_time_stop_v1.json"),
|
||||
"execution_integrity_gate_v1": load_json_safe(_TEMP / "execution_integrity_gate_v1.json"),
|
||||
# Phase-6 Python-tool-only
|
||||
"final_judgment_gate_v1": load_json_safe(_TEMP / "final_judgment_gate_v1.json"),
|
||||
"verdict_consistency_lock_v1": load_json_safe(_TEMP / "verdict_consistency_lock_v1.json"),
|
||||
"data_quality_reconciliation_v1": load_json_safe(_TEMP / "data_quality_reconciliation_v1.json"),
|
||||
}
|
||||
cell_cov = measure_cell_coverage(reg, report_json, hctx, temp_outputs)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Python-tool-only formulas: not in GAS (implemented as Python tools)
|
||||
_PYTHON_TOOL_FORMULAS = {
|
||||
# Phase-1
|
||||
"BLANK_CELL_AUDIT_V1", "VALUE_PRESERVATION_SCORER_V1",
|
||||
"SMART_CASH_RECOVERY_V3", "RATCHET_TRAILING_GENERAL_V1",
|
||||
"EJCE_VIEW_RENDERER_V1", "ROUTING_EXECUTION_LOG_TABLE_V1",
|
||||
# Phase-2
|
||||
"FUNDAMENTAL_RAW_INGEST_V1", "FUNDAMENTAL_MULTIFACTOR_V3",
|
||||
"HORIZON_CLASSIFICATION_V1",
|
||||
# Phase-2B
|
||||
"EARNINGS_QUALITY_SIGNAL_V1", "GROWTH_RATE_SIGNAL_V1",
|
||||
"CASHFLOW_QUALITY_SIGNAL_V1",
|
||||
# Phase-3
|
||||
"SMART_MONEY_FLOW_SIGNAL_V2", "LIQUIDITY_FLOW_SIGNAL_V1",
|
||||
"PORTFOLIO_ALPHA_CONFIDENCE_PER_TICKER_V1",
|
||||
# Phase-3 Market Share V2
|
||||
"MARKET_SHARE_SIGNAL_V2",
|
||||
# [Advanced Harness Architecture]
|
||||
"DYNAMIC_VALUE_PRESERVATION_SELL_V6", "PREDICTIVE_ALPHA_DIALECTIC_ENGINE_V2",
|
||||
"CAPITAL_STYLE_TIME_STOP_V1", "EXECUTION_INTEGRITY_GATE_V1",
|
||||
# Phase-4~5 Python-tool-only 공식 (GAS 구현 없음, Python tools로 구현)
|
||||
"TRADE_QUALITY_FROM_T5_V1", "PREDICTION_ACCURACY_HARNESS_V2",
|
||||
"MACRO_EVENT_TICKER_IMPACT_V1", "SELL_WATERFALL_ENGINE_V2",
|
||||
"LLM_NARRATIVE_TEMPLATE_LOCK_V1", "EJCE_DIVERGENCE_AUDIT_V1",
|
||||
"PREDICTIVE_ALPHA_REPORT_LOCK_V2",
|
||||
# Phase-6 Python-tool-only 공식 (판단 결정론 계층)
|
||||
"FINAL_JUDGMENT_GATE_V1", "VERDICT_CONSISTENCY_LOCK_V1",
|
||||
"INVESTMENT_QUALITY_HEADLINE_V1",
|
||||
# Phase-7 단일 진실원천 + 교차섹션 정합성 (Python-tool-only, GAS 구현 불필요)
|
||||
"CANONICAL_METRICS_V1", "CROSS_SECTION_CONSISTENCY_V1",
|
||||
# Work 7 + Work 3 분석 도구
|
||||
"ALPHA_FEEDBACK_LOOP_V2", "ALPHA_LEAD_THRESHOLD_OPTIMIZER_V1",
|
||||
# Registry sync: formulas implemented outside GAS coverage path
|
||||
"VELOCITY_V1", "PROFIT_LOCK_STAGE_V1", "ANTI_LATE_ENTRY_GATE_V2",
|
||||
"DYNAMIC_HEAT_GATE_V1", "POSITION_SIZE_REGIME_SCALE_V1",
|
||||
"REGIME_CASH_UPLIFT_V1", "DRAWDOWN_GUARD_V1", "POSITION_COUNT_LIMIT_V1",
|
||||
"CASH_FLOOR_V1", "SEMICONDUCTOR_CLUSTER_GATE_V1",
|
||||
"SINGLE_POSITION_WEIGHT_CAP_V1", "REGIME_TRIM_GUIDANCE_V1",
|
||||
"HEAT_CONCENTRATION_ALERT_V1", "SECTOR_CONCENTRATION_LIMIT_V1",
|
||||
"PORTFOLIO_DRAWDOWN_GATE_V1", "K2_STAGED_REBOUND_SELL_V1",
|
||||
"STOP_BREACH_ALERT_V1", "SECTOR_ROTATION_MOMENTUM_V1",
|
||||
"ANTI_WHIPSAW_GATE_V1", "BREAKEVEN_RATCHET_V1",
|
||||
"MARKET_WEIGHT_AWARE_CLUSTER_GATE_V1", "LEADER_POSITION_WEIGHT_CAP_V1",
|
||||
"CAPITAL_STYLE_ALLOCATION_V1",
|
||||
# ENGINE_AUDIT_V1 — Python-tool-only 감사 게이트 (GAS 런타임 비개입)
|
||||
"IMPUTED_DATA_EXPOSURE_GATE_V1",
|
||||
"SCORES_HARNESS_V1",
|
||||
"STRATEGY_ROUTING_AUDIT_V1",
|
||||
"SELL_ENGINE_AUDIT_V1",
|
||||
"YAML_TO_CODE_COVERAGE_V1",
|
||||
"REALIZED_PERFORMANCE_V1",
|
||||
"BACKTEST_HARNESS_V1",
|
||||
# NF1~NF5: GAS execution_order 제외 Python-harness 전용 보조 공식 (python_harness_supplements 등록)
|
||||
"REGIME_CONDITIONAL_MACRO_FACTOR_V1", # NF1 — tools/build_predictive_alpha_dialectic_engine_v2.py
|
||||
"REBOUND_CAPTURE_THESIS_FACTOR_V1", # NF2 — tools/build_predictive_alpha_dialectic_engine_v2.py
|
||||
"ENTRY_TIMING_DECILE_FACTOR_V1", # NF3 — tools/build_late_chase_attribution_v1.py
|
||||
"SELL_SLIPPAGE_BUDGET_FACTOR_V1", # NF4 — tools/build_value_preservation_scorer_v1.py
|
||||
"PROFIT_GIVEBACK_RATCHET_FACTOR_V1", # NF5 — tools/build_ratchet_trailing_general_v1.py
|
||||
# Phase-execution Python-tool-only (tools/build_execution_method_ladder_v1.py, runtime=PYTHON)
|
||||
"EXECUTION_METHOD_LADDER_V1",
|
||||
}
|
||||
# V9 orphan reconcile — _ORPHAN_RECONCILE 버전 태그 공식은 GAS 요구사항 면제
|
||||
ids_to_skip = {fid for fid, fdef in reg.items() if isinstance(fdef, dict) and str(fdef.get("version", "")).endswith("_ORPHAN_RECONCILE")}
|
||||
_PYTHON_TOOL_FORMULAS = _PYTHON_TOOL_FORMULAS | ids_to_skip
|
||||
block_gs_miss = [f for f in gs_miss if f not in _PYTHON_TOOL_FORMULAS]
|
||||
summary = {
|
||||
"formula_total": len(ids),
|
||||
"gs_covered": len(gs_hit),
|
||||
"gs_missing": gs_miss,
|
||||
"gs_coverage_pct": gs_pct,
|
||||
"gs_blocking_missing": block_gs_miss,
|
||||
"ps_required_hooks": [name for name, _ in ps_required_hooks],
|
||||
"ps_hook_covered": len(ps_hook_hit),
|
||||
"ps_hook_missing": ps_hook_miss,
|
||||
"ps_coverage_pct": ps_pct,
|
||||
"cell_coverage": cell_cov,
|
||||
"status": "OK" if (gs_pct >= 100.0 and ps_pct == 100.0 and cell_cov["cell_gate"] != "FAIL") else "FAIL",
|
||||
}
|
||||
|
||||
out = Path(args.output_json)
|
||||
if not out.is_absolute():
|
||||
out = ROOT / out
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
print(
|
||||
f"YAML_GS_PS_COVERAGE: gs={gs_pct:.2f}% "
|
||||
f"ps={ps_pct:.2f}% total={len(ids)} "
|
||||
f"cell_coverage={cell_cov['cell_coverage_pct']:.2f}% [{cell_cov['cell_gate']}]"
|
||||
)
|
||||
if summary["status"] == "OK":
|
||||
print("YAML_GS_PS_COVERAGE_OK")
|
||||
return 0
|
||||
print("YAML_GS_PS_COVERAGE_FAIL")
|
||||
if args.strict_100:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1 @@
|
||||
"""Auto-generated quant_engine.models package."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Auto-generated schema model package."""
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ABSOLUTE_RISK_STOP_V1'
|
||||
SCHEMA_ID = 'schema://formula/ABSOLUTE_RISK_STOP_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/absolute_risk_stop_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ABSOLUTE_RISK_STOP_V1",
|
||||
"title": "ABSOLUTE_RISK_STOP_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ABSOLUTE_RISK_STOP_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"holdings",
|
||||
"df_map"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ALGORITHM_GUIDANCE_PROOF_V1'
|
||||
SCHEMA_ID = 'schema://formula/ALGORITHM_GUIDANCE_PROOF_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/algorithm_guidance_proof_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ALGORITHM_GUIDANCE_PROOF_V1",
|
||||
"title": "ALGORITHM_GUIDANCE_PROOF_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ALGORITHM_GUIDANCE_PROOF_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ALPHA_EVALUATION_WINDOW_V1'
|
||||
SCHEMA_ID = 'schema://formula/ALPHA_EVALUATION_WINDOW_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/alpha_evaluation_window_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ALPHA_EVALUATION_WINDOW_V1",
|
||||
"title": "ALPHA_EVALUATION_WINDOW_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ALPHA_EVALUATION_WINDOW_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"entry_date",
|
||||
"position_class",
|
||||
"t20_return_pct",
|
||||
"t60_return_pct",
|
||||
"benchmark_core_return_pct"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ALPHA_FEEDBACK_LOOP_V1'
|
||||
SCHEMA_ID = 'schema://formula/ALPHA_FEEDBACK_LOOP_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/alpha_feedback_loop_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ALPHA_FEEDBACK_LOOP_V1",
|
||||
"title": "ALPHA_FEEDBACK_LOOP_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ALPHA_FEEDBACK_LOOP_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"alpha_evaluation_window_json",
|
||||
"saqg_v1",
|
||||
"brt_verdict",
|
||||
"market_regime"
|
||||
],
|
||||
"x_formula_outputs": [
|
||||
{
|
||||
"field": "alpha_feedback_json",
|
||||
"subfields": [
|
||||
"eligible_t20_fail_rate",
|
||||
"eligible_t60_fail_rate",
|
||||
"recommended_filter_adjustments",
|
||||
"cases_analyzed"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ANTI_CHASE_V1'
|
||||
SCHEMA_ID = 'schema://formula/ANTI_CHASE_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/anti_chase_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ANTI_CHASE_V1",
|
||||
"title": "ANTI_CHASE_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ANTI_CHASE_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ANTI_CHASING_VELOCITY_V1'
|
||||
SCHEMA_ID = 'schema://formula/ANTI_CHASING_VELOCITY_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/anti_chasing_velocity_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ANTI_CHASING_VELOCITY_V1",
|
||||
"title": "ANTI_CHASING_VELOCITY_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ANTI_CHASING_VELOCITY_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"close",
|
||||
"close_1d_ago",
|
||||
"close_5d_ago",
|
||||
"market_regime"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ANTI_LATE_ENTRY_GATE_V2'
|
||||
SCHEMA_ID = 'schema://formula/ANTI_LATE_ENTRY_GATE_V2'
|
||||
SCHEMA_PATH = 'schemas/generated/anti_late_entry_gate_v2.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ANTI_LATE_ENTRY_GATE_V2",
|
||||
"title": "ANTI_LATE_ENTRY_GATE_V2",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ANTI_LATE_ENTRY_GATE_V2"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ANTI_WHIPSAW_GATE_V1'
|
||||
SCHEMA_ID = 'schema://formula/ANTI_WHIPSAW_GATE_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/anti_whipsaw_gate_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ANTI_WHIPSAW_GATE_V1",
|
||||
"title": "ANTI_WHIPSAW_GATE_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ANTI_WHIPSAW_GATE_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"close_price",
|
||||
"ma20",
|
||||
"rsi14"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'ARTIFACT_FRESHNESS_GATE_V1'
|
||||
SCHEMA_ID = 'schema://formula/ARTIFACT_FRESHNESS_GATE_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/artifact_freshness_gate_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/ARTIFACT_FRESHNESS_GATE_V1",
|
||||
"title": "ARTIFACT_FRESHNESS_GATE_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "ARTIFACT_FRESHNESS_GATE_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'AUDIT_REPLAY_SNAPSHOT_V1'
|
||||
SCHEMA_ID = 'schema://formula/AUDIT_REPLAY_SNAPSHOT_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/audit_replay_snapshot_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/AUDIT_REPLAY_SNAPSHOT_V1",
|
||||
"title": "AUDIT_REPLAY_SNAPSHOT_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "AUDIT_REPLAY_SNAPSHOT_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'BENCHMARK_RELATIVE_TIMESERIES_V1'
|
||||
SCHEMA_ID = 'schema://formula/BENCHMARK_RELATIVE_TIMESERIES_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/benchmark_relative_timeseries_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/BENCHMARK_RELATIVE_TIMESERIES_V1",
|
||||
"title": "BENCHMARK_RELATIVE_TIMESERIES_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "BENCHMARK_RELATIVE_TIMESERIES_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"price.ret5D",
|
||||
"price.ret20D",
|
||||
"price.ret60D",
|
||||
"price.close",
|
||||
"high52w",
|
||||
"globalKospiRet5D_",
|
||||
"globalKospiRet20D_",
|
||||
"globalKospiRet60D_",
|
||||
"globalKospiDrawdown_"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'BLANK_CELL_AUDIT_V1'
|
||||
SCHEMA_ID = 'schema://formula/BLANK_CELL_AUDIT_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/blank_cell_audit_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/BLANK_CELL_AUDIT_V1",
|
||||
"title": "BLANK_CELL_AUDIT_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "BLANK_CELL_AUDIT_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"operational_report_json"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'BREAKEVEN_RATCHET_V1'
|
||||
SCHEMA_ID = 'schema://formula/BREAKEVEN_RATCHET_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/breakeven_ratchet_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/BREAKEVEN_RATCHET_V1",
|
||||
"title": "BREAKEVEN_RATCHET_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "BREAKEVEN_RATCHET_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"average_cost",
|
||||
"highest_price_since_entry"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'BREAKOUT_QUALITY_GATE_V2'
|
||||
SCHEMA_ID = 'schema://formula/BREAKOUT_QUALITY_GATE_V2'
|
||||
SCHEMA_PATH = 'schemas/generated/breakout_quality_gate_v2.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/BREAKOUT_QUALITY_GATE_V2",
|
||||
"title": "BREAKOUT_QUALITY_GATE_V2",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "BREAKOUT_QUALITY_GATE_V2"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"close",
|
||||
"ma20",
|
||||
"ret_3d",
|
||||
"ret_1d",
|
||||
"disparity",
|
||||
"rsi14",
|
||||
"volume",
|
||||
"avg_volume_5d",
|
||||
"timing_score_exit",
|
||||
"distribution_risk_score",
|
||||
"late_chase_risk_score"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CANONICAL_ARTIFACT_RESOLVER_V1'
|
||||
SCHEMA_ID = 'schema://formula/CANONICAL_ARTIFACT_RESOLVER_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/canonical_artifact_resolver_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CANONICAL_ARTIFACT_RESOLVER_V1",
|
||||
"title": "CANONICAL_ARTIFACT_RESOLVER_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CANONICAL_ARTIFACT_RESOLVER_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CANONICAL_METRICS_V1'
|
||||
SCHEMA_ID = 'schema://formula/CANONICAL_METRICS_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/canonical_metrics_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CANONICAL_METRICS_V1",
|
||||
"title": "CANONICAL_METRICS_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CANONICAL_METRICS_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CAPITAL_STYLE_ALLOCATION_V1'
|
||||
SCHEMA_ID = 'schema://formula/CAPITAL_STYLE_ALLOCATION_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/capital_style_allocation_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CAPITAL_STYLE_ALLOCATION_V1",
|
||||
"title": "CAPITAL_STYLE_ALLOCATION_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CAPITAL_STYLE_ALLOCATION_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"smart_money_flow_signal_v2_json",
|
||||
"fundamental_multifactor_v3_json",
|
||||
"macro_event_ticker_impact_v1_json",
|
||||
"liquidity_flow_signal_v1_json"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASH_CREATION_PURPOSE_LOCK_V1'
|
||||
SCHEMA_ID = 'schema://formula/CASH_CREATION_PURPOSE_LOCK_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cash_creation_purpose_lock_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASH_CREATION_PURPOSE_LOCK_V1",
|
||||
"title": "CASH_CREATION_PURPOSE_LOCK_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASH_CREATION_PURPOSE_LOCK_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"composite_verdict",
|
||||
"rs_verdict",
|
||||
"brt_verdict",
|
||||
"excess_drawdown_pctp",
|
||||
"recovery_ratio_20d",
|
||||
"sfg_v1"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASH_FLOOR_V1'
|
||||
SCHEMA_ID = 'schema://formula/CASH_FLOOR_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cash_floor_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASH_FLOOR_V1",
|
||||
"title": "CASH_FLOOR_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASH_FLOOR_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"total_asset",
|
||||
"settlement_cash_d2_krw",
|
||||
"market_risk_score"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASH_RAISE_PARETO_EXECUTOR_V2'
|
||||
SCHEMA_ID = 'schema://formula/CASH_RAISE_PARETO_EXECUTOR_V2'
|
||||
SCHEMA_PATH = 'schemas/generated/cash_raise_pareto_executor_v2.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASH_RAISE_PARETO_EXECUTOR_V2",
|
||||
"title": "CASH_RAISE_PARETO_EXECUTOR_V2",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASH_RAISE_PARETO_EXECUTOR_V2"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASH_RAISE_VALUE_OPTIMIZER_V3'
|
||||
SCHEMA_ID = 'schema://formula/CASH_RAISE_VALUE_OPTIMIZER_V3'
|
||||
SCHEMA_PATH = 'schemas/generated/cash_raise_value_optimizer_v3.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASH_RAISE_VALUE_OPTIMIZER_V3",
|
||||
"title": "CASH_RAISE_VALUE_OPTIMIZER_V3",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASH_RAISE_VALUE_OPTIMIZER_V3"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASH_RATIOS_V1'
|
||||
SCHEMA_ID = 'schema://formula/CASH_RATIOS_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cash_ratios_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASH_RATIOS_V1",
|
||||
"title": "CASH_RATIOS_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASH_RATIOS_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"settlement_cash",
|
||||
"reserved_order_amount",
|
||||
"planned_buy_amount",
|
||||
"sell_cash_proceeds_d2",
|
||||
"total_asset"
|
||||
],
|
||||
"x_formula_outputs": {
|
||||
"settlement_cash_ratio": "settlement_cash / total_asset * 100",
|
||||
"total_cash_ratio": "settlement_cash / total_asset * 100",
|
||||
"buy_power_cash": "settlement_cash - reserved_order_amount",
|
||||
"buy_power_ratio": "(settlement_cash - reserved_order_amount) / total_asset * 100",
|
||||
"post_trade_total_cash_ratio": "(settlement_cash - planned_buy_amount + sell_cash_proceeds_d2) / total_asset * 100"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASH_RECOVERY_OPTIMIZER_V1'
|
||||
SCHEMA_ID = 'schema://formula/CASH_RECOVERY_OPTIMIZER_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cash_recovery_optimizer_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASH_RECOVERY_OPTIMIZER_V1",
|
||||
"title": "CASH_RECOVERY_OPTIMIZER_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASH_RECOVERY_OPTIMIZER_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"cash_shortfall_target_krw",
|
||||
"cash_shortfall_min_krw",
|
||||
"sell_candidates_json",
|
||||
"immediate_sell_qty",
|
||||
"sell_limit_price",
|
||||
"holding_qty"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASH_RECOVERY_OPTIMIZER_V4'
|
||||
SCHEMA_ID = 'schema://formula/CASH_RECOVERY_OPTIMIZER_V4'
|
||||
SCHEMA_PATH = 'schemas/generated/cash_recovery_optimizer_v4.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASH_RECOVERY_OPTIMIZER_V4",
|
||||
"title": "CASH_RECOVERY_OPTIMIZER_V4",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASH_RECOVERY_OPTIMIZER_V4"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASH_RECOVERY_V1'
|
||||
SCHEMA_ID = 'schema://formula/CASH_RECOVERY_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cash_recovery_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASH_RECOVERY_V1",
|
||||
"title": "CASH_RECOVERY_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASH_RECOVERY_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASHFLOW_QUALITY_SIGNAL_V1'
|
||||
SCHEMA_ID = 'schema://formula/CASHFLOW_QUALITY_SIGNAL_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cashflow_quality_signal_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASHFLOW_QUALITY_SIGNAL_V1",
|
||||
"title": "CASHFLOW_QUALITY_SIGNAL_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASHFLOW_QUALITY_SIGNAL_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CASHFLOW_STABILITY_GATE_V1'
|
||||
SCHEMA_ID = 'schema://formula/CASHFLOW_STABILITY_GATE_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cashflow_stability_gate_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CASHFLOW_STABILITY_GATE_V1",
|
||||
"title": "CASHFLOW_STABILITY_GATE_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CASHFLOW_STABILITY_GATE_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"operating_cf_krw",
|
||||
"free_cf_krw",
|
||||
"accrual_ratio_pct"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CLA_REGIME_EXIT_CONDITION_V1'
|
||||
SCHEMA_ID = 'schema://formula/CLA_REGIME_EXIT_CONDITION_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cla_regime_exit_condition_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CLA_REGIME_EXIT_CONDITION_V1",
|
||||
"title": "CLA_REGIME_EXIT_CONDITION_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CLA_REGIME_EXIT_CONDITION_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"ticker",
|
||||
"rs_verdict",
|
||||
"brt_verdict",
|
||||
"frg_5d_sh",
|
||||
"volume",
|
||||
"avg_volume_5d",
|
||||
"market_regime"
|
||||
],
|
||||
"x_formula_outputs": [
|
||||
{
|
||||
"field": "cla_exit_status",
|
||||
"unit": "enum [CLA_ACTIVE,CLA_EXIT_WARNING,CLA_EXIT_CONFIRMED]"
|
||||
},
|
||||
{
|
||||
"field": "cla_exit_signals_triggered",
|
||||
"unit": "list"
|
||||
},
|
||||
{
|
||||
"field": "cla_exit_total_weight",
|
||||
"unit": "int"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'COMPLETION_GAP_V1'
|
||||
SCHEMA_ID = 'schema://formula/COMPLETION_GAP_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/completion_gap_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/COMPLETION_GAP_V1",
|
||||
"title": "COMPLETION_GAP_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "COMPLETION_GAP_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'COMPOSITE_VERDICT_V1'
|
||||
SCHEMA_ID = 'schema://formula/COMPOSITE_VERDICT_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/composite_verdict_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/COMPOSITE_VERDICT_V1",
|
||||
"title": "COMPOSITE_VERDICT_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "COMPOSITE_VERDICT_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [
|
||||
"ss001_grade",
|
||||
"rs_verdict"
|
||||
],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'COMPREHENSIVE_PROPOSAL_V1'
|
||||
SCHEMA_ID = 'schema://formula/COMPREHENSIVE_PROPOSAL_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/comprehensive_proposal_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/COMPREHENSIVE_PROPOSAL_V1",
|
||||
"title": "COMPREHENSIVE_PROPOSAL_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "COMPREHENSIVE_PROPOSAL_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CONTINUOUS_EVALUATION_DASHBOARD_V1'
|
||||
SCHEMA_ID = 'schema://formula/CONTINUOUS_EVALUATION_DASHBOARD_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/continuous_evaluation_dashboard_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CONTINUOUS_EVALUATION_DASHBOARD_V1",
|
||||
"title": "CONTINUOUS_EVALUATION_DASHBOARD_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CONTINUOUS_EVALUATION_DASHBOARD_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'CROSS_SECTION_CONSISTENCY_V1'
|
||||
SCHEMA_ID = 'schema://formula/CROSS_SECTION_CONSISTENCY_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/cross_section_consistency_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CROSS_SECTION_CONSISTENCY_V1",
|
||||
"title": "CROSS_SECTION_CONSISTENCY_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "CROSS_SECTION_CONSISTENCY_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'DATA_INTEGRITY_100_LOCK_V1'
|
||||
SCHEMA_ID = 'schema://formula/DATA_INTEGRITY_100_LOCK_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/data_integrity_100_lock_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/DATA_INTEGRITY_100_LOCK_V1",
|
||||
"title": "DATA_INTEGRITY_100_LOCK_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "DATA_INTEGRITY_100_LOCK_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'DATA_INTEGRITY_100_LOCK_V2'
|
||||
SCHEMA_ID = 'schema://formula/DATA_INTEGRITY_100_LOCK_V2'
|
||||
SCHEMA_PATH = 'schemas/generated/data_integrity_100_lock_v2.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/DATA_INTEGRITY_100_LOCK_V2",
|
||||
"title": "DATA_INTEGRITY_100_LOCK_V2",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "DATA_INTEGRITY_100_LOCK_V2"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'DATA_INTEGRITY_SCORE_V1'
|
||||
SCHEMA_ID = 'schema://formula/DATA_INTEGRITY_SCORE_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/data_integrity_score_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/DATA_INTEGRITY_SCORE_V1",
|
||||
"title": "DATA_INTEGRITY_SCORE_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "DATA_INTEGRITY_SCORE_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Auto-generated schema model descriptor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_TITLE = 'DATA_MATURITY_TRUTH_GATE_V1'
|
||||
SCHEMA_ID = 'schema://formula/DATA_MATURITY_TRUTH_GATE_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/data_maturity_truth_gate_v1.schema.json'
|
||||
SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs']
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaModel:
|
||||
title: str
|
||||
schema_id: str
|
||||
path: str
|
||||
properties: list[str]
|
||||
required: list[str]
|
||||
|
||||
def load_schema() -> dict[str, Any]:
|
||||
return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))
|
||||
|
||||
def describe() -> SchemaModel:
|
||||
return SchemaModel(
|
||||
title=SCHEMA_TITLE,
|
||||
schema_id=SCHEMA_ID,
|
||||
path=SCHEMA_PATH,
|
||||
properties=list(SCHEMA_PROPERTIES),
|
||||
required=list(SCHEMA_REQUIRED),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/DATA_MATURITY_TRUTH_GATE_V1",
|
||||
"title": "DATA_MATURITY_TRUTH_GATE_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": {
|
||||
"const": "DATA_MATURITY_TRUTH_GATE_V1"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"formula_id",
|
||||
"owner",
|
||||
"status",
|
||||
"inputs",
|
||||
"outputs"
|
||||
],
|
||||
"x_formula_inputs": [],
|
||||
"x_formula_outputs": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user