feat(quant-engine): 10개 고전 기술전략 갭분석 후 7개 보조신호 채택
사용자 제시 10개 고전 기술전략(골든크로스/모멘텀/52주신고가/연속상승하락/이격도/돌파실패/ 강한종가/변동성확장/평균회귀/추세필터)을 기존 엔진과 대조한 갭분석 결과: - 이미 구현됨: 모멘텀(VELOCITY_V1/RS_MOMENTUM_V1), 이격도·평균회귀(MEAN_REVERSION_GATE_V1) - 신규 채택 7개: GOLDEN_CROSS_SIGNAL_V1, STRONG_CLOSE_SIGNAL_V1, VOLATILITY_EXPANSION_BREAKOUT_V1, FIFTY_TWO_WEEK_HIGH_TRIGGER_V1, CONSECUTIVE_STREAK_V1, BREAKOUT_FAILURE_STOP_V1, TREND_FILTER_GATE_V1 AGENTS.md 하드룰(추격매수 방지, anti-late-entry gate 필수통과)에 따라 BUY 방향 신호 전부를 STRATEGY_SCORING의 보조신호로만 편입 — BREAKOUT_QUALITY_GATE_V2/FOLLOW_THROUGH_DAY_CONFIRM_V1/ ANTI_LATE_ENTRY_GATE_V2 게이트 체인을 우회하는 독립 BUY 트리거로는 사용하지 않음. 검증: validate_specs/validate_golden_coverage_100(100%)/validate_calibration_registry_v1/ validate_schema_model_generation_v1/validate_agents_shrink_v1 전부 PASS. golden test 22/22 PASS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
schema_version: technical_signals_p4_adoption_plan.v1
|
||||
meta:
|
||||
title: technical_signals_p4_adoption_plan
|
||||
source: 사용자 제시 10개 고전 기술적 전략 목록 (2026-06-18)
|
||||
decision_basis: >
|
||||
10개 전략 중 기존 엔진 커버리지를 분석한 결과 3개는 이미 구현됨(모멘텀=VELOCITY_V1/RS_MOMENTUM_V1,
|
||||
이격도/평균회귀=MEAN_REVERSION_GATE_V1), 4개는 부분구현, 3개는 완전 공백.
|
||||
AGENTS.md 하드룰(추격매수 방지, anti-late-entry gate 필수통과)과 충돌하지 않도록
|
||||
이 7개를 독립 매수 트리거가 아니라 STRATEGY_SCORING 보조신호 또는 기존 게이트 체인 내부
|
||||
조건으로만 편입한다.
|
||||
gap_analysis:
|
||||
already_covered:
|
||||
"02_모멘텀": VELOCITY_V1, RS_MOMENTUM_V1
|
||||
"05_이격도": MEAN_REVERSION_GATE_V1
|
||||
"09_평균회귀": MEAN_REVERSION_GATE_V1
|
||||
partial_extend:
|
||||
"03_52주신고가": FIFTY_TWO_WEEK_HIGH_TRIGGER_V1
|
||||
"04_연속상승하락": CONSECUTIVE_STREAK_V1
|
||||
"06_돌파실패손절": BREAKOUT_FAILURE_STOP_V1
|
||||
"10_추세필터": TREND_FILTER_GATE_V1
|
||||
new_signal:
|
||||
"01_골든크로스": GOLDEN_CROSS_SIGNAL_V1
|
||||
"07_강한종가": STRONG_CLOSE_SIGNAL_V1
|
||||
"08_변동성확장돌파": VOLATILITY_EXPANSION_BREAKOUT_V1
|
||||
hard_constraint: >
|
||||
이 7개 공식 중 BUY 방향 신호는 모두 BREAKOUT_QUALITY_GATE_V2 / FOLLOW_THROUGH_DAY_CONFIRM_V1 /
|
||||
ANTI_LATE_ENTRY_GATE_V2 체인을 우회할 수 없다. 단독 BUY 트리거로 사용 금지 — STRATEGY_SCORING의
|
||||
component_scores 보조 입력 또는 게이트 조건으로만 사용한다.
|
||||
|
||||
tasks:
|
||||
- id: P4-1
|
||||
title: GOLDEN_CROSS_SIGNAL_V1
|
||||
output_file: spec/formulas/domains/entry.yaml
|
||||
implementation: tools/build_golden_cross_signal_v1.py
|
||||
- id: P4-2
|
||||
title: STRONG_CLOSE_SIGNAL_V1
|
||||
output_file: spec/formulas/domains/entry.yaml
|
||||
implementation: tools/build_strong_close_signal_v1.py
|
||||
- id: P4-3
|
||||
title: VOLATILITY_EXPANSION_BREAKOUT_V1
|
||||
output_file: spec/formulas/domains/entry.yaml
|
||||
detail: bb_width 신규 필드 추가. BREAKOUT_QUALITY_GATE_V2 통과 필수 조건 명시.
|
||||
implementation: tools/build_volatility_expansion_breakout_v1.py
|
||||
- id: P4-4
|
||||
title: FIFTY_TWO_WEEK_HIGH_TRIGGER_V1
|
||||
output_file: spec/formulas/domains/entry.yaml
|
||||
implementation: tools/build_fifty_two_week_high_trigger_v1.py
|
||||
- id: P4-5
|
||||
title: CONSECUTIVE_STREAK_V1
|
||||
output_file: spec/formulas/domains/entry.yaml
|
||||
detail: up_streak/down_streak 대칭 공식화 (down_streak는 기존 필드, up_streak 신규).
|
||||
implementation: tools/build_consecutive_streak_v1.py
|
||||
- id: P4-6
|
||||
title: BREAKOUT_FAILURE_STOP_V1
|
||||
output_file: spec/formulas/domains/exit.yaml
|
||||
implementation: tools/build_breakout_failure_stop_v1.py
|
||||
- id: P4-7
|
||||
title: TREND_FILTER_GATE_V1
|
||||
output_file: spec/formulas/domains/entry.yaml
|
||||
implementation: tools/build_trend_filter_gate_v1.py
|
||||
- id: P4-8
|
||||
title: decision_flow/manifest 배선 + 전체 검증
|
||||
depends_on: [P4-1, P4-2, P4-3, P4-4, P4-5, P4-6, P4-7]
|
||||
command: |
|
||||
python tools/validate_specs.py
|
||||
python tools/validate_golden_coverage_100.py
|
||||
python tools/validate_calibration_registry_v1.py
|
||||
python tools/validate_schema_model_generation_v1.py
|
||||
python tools/validate_agents_shrink_v1.py
|
||||
@@ -18,6 +18,13 @@ source_precedence:
|
||||
- model_governance_kill_switch_v1
|
||||
- state_vector_constructor_v1
|
||||
- weekly_legacy_transfer_plan_v1
|
||||
- golden_cross_signal_v1
|
||||
- strong_close_signal_v1
|
||||
- volatility_expansion_breakout_v1
|
||||
- fifty_two_week_high_trigger_v1
|
||||
- consecutive_streak_v1
|
||||
- breakout_failure_stop_v1
|
||||
- trend_filter_gate_v1
|
||||
- portfolio_transition_optimizer_v1
|
||||
- walk_forward_bootstrap_v1
|
||||
- transition_set_enumerator_v1
|
||||
@@ -90,3 +97,24 @@ manifest_rows:
|
||||
- formula_id: weekly_legacy_transfer_plan_v1
|
||||
active_artifact: Temp/weekly_legacy_transfer_plan_v1.json
|
||||
value: 0.0
|
||||
- formula_id: golden_cross_signal_v1
|
||||
active_artifact: Temp/golden_cross_signal_v1.json
|
||||
value: null
|
||||
- formula_id: strong_close_signal_v1
|
||||
active_artifact: Temp/strong_close_signal_v1.json
|
||||
value: null
|
||||
- formula_id: volatility_expansion_breakout_v1
|
||||
active_artifact: Temp/volatility_expansion_breakout_v1.json
|
||||
value: null
|
||||
- formula_id: fifty_two_week_high_trigger_v1
|
||||
active_artifact: Temp/fifty_two_week_high_trigger_v1.json
|
||||
value: null
|
||||
- formula_id: consecutive_streak_v1
|
||||
active_artifact: Temp/consecutive_streak_v1.json
|
||||
value: null
|
||||
- formula_id: breakout_failure_stop_v1
|
||||
active_artifact: Temp/breakout_failure_stop_v1.json
|
||||
value: null
|
||||
- formula_id: trend_filter_gate_v1
|
||||
active_artifact: Temp/trend_filter_gate_v1.json
|
||||
value: null
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/BREAKOUT_FAILURE_STOP_V1",
|
||||
"title": "BREAKOUT_FAILURE_STOP_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "BREAKOUT_FAILURE_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": ["prior_high", "close_price", "days_since_breakout"],
|
||||
"x_formula_outputs": ["breakout_failure"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CONSECUTIVE_STREAK_V1",
|
||||
"title": "CONSECUTIVE_STREAK_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "CONSECUTIVE_STREAK_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": ["daily_close_changes"],
|
||||
"x_formula_outputs": ["up_streak", "down_streak"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/FIFTY_TWO_WEEK_HIGH_TRIGGER_V1",
|
||||
"title": "FIFTY_TWO_WEEK_HIGH_TRIGGER_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "FIFTY_TWO_WEEK_HIGH_TRIGGER_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", "high52w"],
|
||||
"x_formula_outputs": ["fifty_two_week_high_breakout"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/GOLDEN_CROSS_SIGNAL_V1",
|
||||
"title": "GOLDEN_CROSS_SIGNAL_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "GOLDEN_CROSS_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": ["ma20", "ma20_prev", "ma60", "ma60_prev"],
|
||||
"x_formula_outputs": ["golden_cross_today"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/STRONG_CLOSE_SIGNAL_V1",
|
||||
"title": "STRONG_CLOSE_SIGNAL_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "STRONG_CLOSE_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": ["close_price", "high_price", "low_price"],
|
||||
"x_formula_outputs": ["strong_close", "close_position_pct"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/TREND_FILTER_GATE_V1",
|
||||
"title": "TREND_FILTER_GATE_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "TREND_FILTER_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", "ma120", "ma120_prev"],
|
||||
"x_formula_outputs": ["trend_filter_pass"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/VOLATILITY_EXPANSION_BREAKOUT_V1",
|
||||
"title": "VOLATILITY_EXPANSION_BREAKOUT_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "VOLATILITY_EXPANSION_BREAKOUT_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": ["bb_width", "bb_width_20d_percentile", "ret_1d"],
|
||||
"x_formula_outputs": ["volatility_expansion_breakout"]
|
||||
}
|
||||
@@ -106,13 +106,24 @@ decision_flow:
|
||||
pass_condition: "market regime classified or marked UNKNOWN"
|
||||
fail_state: "INSUFFICIENT_DATA"
|
||||
STRATEGY_SCORING:
|
||||
purpose: "섹터/종목/수급/유동성/실적/밸류 점수 산출"
|
||||
purpose: >
|
||||
섹터/종목/수급/유동성/실적/밸류 점수 산출. GOLDEN_CROSS_SIGNAL_V1, STRONG_CLOSE_SIGNAL_V1,
|
||||
VOLATILITY_EXPANSION_BREAKOUT_V1, FIFTY_TWO_WEEK_HIGH_TRIGGER_V1, CONSECUTIVE_STREAK_V1,
|
||||
TREND_FILTER_GATE_V1은 component_scores의 보조신호로만 포함되며, 단독으로 BUY를
|
||||
허가하지 않는다(BREAKOUT_QUALITY_GATE_V2/FOLLOW_THROUGH_DAY_CONFIRM_V1/ANTI_LATE_ENTRY_GATE_V2
|
||||
체인 우회 금지).
|
||||
required_refs:
|
||||
- "spec/08_scoring_rules.yaml:strategy_score"
|
||||
- "spec/strategy/sector_model.yaml:sector_model"
|
||||
- "spec/13_formula_registry.yaml:formula_registry.formulas.FLOW_CREDIT_V1"
|
||||
- "spec/formulas/domains/entry.yaml:GOLDEN_CROSS_SIGNAL_V1"
|
||||
- "spec/formulas/domains/entry.yaml:STRONG_CLOSE_SIGNAL_V1"
|
||||
- "spec/formulas/domains/entry.yaml:VOLATILITY_EXPANSION_BREAKOUT_V1"
|
||||
- "spec/formulas/domains/entry.yaml:FIFTY_TWO_WEEK_HIGH_TRIGGER_V1"
|
||||
- "spec/formulas/domains/entry.yaml:CONSECUTIVE_STREAK_V1"
|
||||
- "spec/formulas/domains/entry.yaml:TREND_FILTER_GATE_V1"
|
||||
required_inputs: ["close_price", "ma20", "flow_ok", "flow_rows", "frg_5d_sh", "inst_5d_sh", "avg_trade_value_5d"]
|
||||
computed_outputs: ["flow_credit", "strategy_score", "component_scores", "grade_candidate"]
|
||||
computed_outputs: ["flow_credit", "strategy_score", "component_scores", "grade_candidate", "golden_cross_today", "strong_close", "volatility_expansion_breakout", "fifty_two_week_high_breakout", "up_streak", "down_streak", "trend_filter_pass"]
|
||||
pass_condition: "strategy_score calculated or missing fields listed"
|
||||
fail_state: "INSUFFICIENT_DATA"
|
||||
PORTFOLIO_CONSTRAINT_CHECK:
|
||||
@@ -179,14 +190,17 @@ decision_flow:
|
||||
pass_condition: "integer quantity calculated, or NO_QUANTITY reason emitted"
|
||||
fail_state: "INSUFFICIENT_DATA"
|
||||
EXIT_POLICY_CHECK:
|
||||
purpose: "손절/익절/trailing/보유주 점검 규칙 적용"
|
||||
purpose: >
|
||||
손절/익절/trailing/보유주 점검 규칙 적용. 돌파 매수로 진입한 포지션에는
|
||||
BREAKOUT_FAILURE_STOP_V1을 적용해 전고점 재이탈을 ANTI_WHIPSAW_GATE_V1과 별도로 포착한다.
|
||||
required_refs:
|
||||
- "spec/exit/stop_loss.yaml:stop_loss"
|
||||
- "spec/exit/take_profit.yaml:take_profit"
|
||||
- "spec/exit/position_review.yaml:position_review_cycle"
|
||||
- "spec/13_formula_registry.yaml:formula_registry.formulas.EXPECTED_EDGE_V1"
|
||||
- "spec/formulas/domains/exit.yaml:BREAKOUT_FAILURE_STOP_V1"
|
||||
required_inputs: ["entry_price", "stop_price", "target_price", "final_quantity"]
|
||||
computed_outputs: ["expected_edge", "stop_order", "take_profit_order", "invalidation_conditions"]
|
||||
computed_outputs: ["expected_edge", "stop_order", "take_profit_order", "invalidation_conditions", "breakout_failure"]
|
||||
pass_condition: "exit or hold policy evaluated"
|
||||
fail_state: "INSUFFICIENT_DATA"
|
||||
proactive_exit_radar_check: # [2026-05-19_PROACTIVE_RADAR_V1]
|
||||
|
||||
@@ -2672,6 +2672,116 @@ field_dictionary:
|
||||
unit: "KRW"
|
||||
aliases: ["TRANSFER_CONFIRMED_AMOUNT_KRW"]
|
||||
note: "WEEKLY_LEGACY_TRANSFER_PLAN_V1 입력 — transfer_confirmed=true일 때만 값 존재."
|
||||
# ── [2026-06-18_TECHNICAL_SIGNALS_P4] 10개 고전 기술전략 갭분석 채택 신규 필드 ──
|
||||
ma20_prev:
|
||||
canonical_name: "ma20_prev"
|
||||
type: "number_or_null"
|
||||
unit: "KRW_per_share"
|
||||
aliases: ["MA20_PREV"]
|
||||
note: "GOLDEN_CROSS_SIGNAL_V1 입력 — 전일 ma20."
|
||||
ma60_prev:
|
||||
canonical_name: "ma60_prev"
|
||||
type: "number_or_null"
|
||||
unit: "KRW_per_share"
|
||||
aliases: ["MA60_PREV"]
|
||||
note: "GOLDEN_CROSS_SIGNAL_V1 입력 — 전일 ma60."
|
||||
ma120:
|
||||
canonical_name: "ma120"
|
||||
type: "number_or_null"
|
||||
unit: "KRW_per_share"
|
||||
aliases: ["MA120", "120일선"]
|
||||
note: "TREND_FILTER_GATE_V1 입력 — 120일 이동평균."
|
||||
ma120_prev:
|
||||
canonical_name: "ma120_prev"
|
||||
type: "number_or_null"
|
||||
unit: "KRW_per_share"
|
||||
aliases: ["MA120_PREV"]
|
||||
note: "TREND_FILTER_GATE_V1 입력 — 전일 ma120."
|
||||
high_price:
|
||||
canonical_name: "high_price"
|
||||
type: "number"
|
||||
unit: "KRW_per_share"
|
||||
aliases: ["High", "고가", "high"]
|
||||
note: "STRONG_CLOSE_SIGNAL_V1 입력 — 당일 고가."
|
||||
low_price:
|
||||
canonical_name: "low_price"
|
||||
type: "number"
|
||||
unit: "KRW_per_share"
|
||||
aliases: ["Low", "저가", "low"]
|
||||
note: "STRONG_CLOSE_SIGNAL_V1 입력 — 당일 저가."
|
||||
bb_width:
|
||||
canonical_name: "bb_width"
|
||||
type: "number_or_null"
|
||||
unit: "percent"
|
||||
aliases: ["BB_WIDTH"]
|
||||
note: "VOLATILITY_EXPANSION_BREAKOUT_V1 입력 — 20일 볼린저밴드 폭."
|
||||
bb_width_20d_percentile:
|
||||
canonical_name: "bb_width_20d_percentile"
|
||||
type: "number_or_null"
|
||||
unit: "percent"
|
||||
aliases: ["BB_WIDTH_20D_PERCENTILE"]
|
||||
note: "VOLATILITY_EXPANSION_BREAKOUT_V1 입력 — 최근 20일 분포 내 bb_width 백분위. 낮을수록 squeeze."
|
||||
daily_close_changes:
|
||||
canonical_name: "daily_close_changes"
|
||||
type: "list_or_null"
|
||||
unit: "list_of_percent"
|
||||
aliases: ["DAILY_CLOSE_CHANGES"]
|
||||
note: "CONSECUTIVE_STREAK_V1 입력 — 최근 N거래일 일별 종가 변화율(%) 리스트, 최신값이 마지막."
|
||||
prior_high:
|
||||
canonical_name: "prior_high"
|
||||
type: "number_or_null"
|
||||
unit: "KRW_per_share"
|
||||
aliases: ["PRIOR_HIGH"]
|
||||
note: "BREAKOUT_FAILURE_STOP_V1 입력 — 진입 당시 돌파 기준 전고점."
|
||||
golden_cross_today:
|
||||
canonical_name: "golden_cross_today"
|
||||
type: "boolean_or_null"
|
||||
unit: "none"
|
||||
aliases: ["GOLDEN_CROSS_TODAY"]
|
||||
note: "GOLDEN_CROSS_SIGNAL_V1 산출 — STRATEGY_SCORING 보조신호. 단독 BUY 트리거 금지."
|
||||
strong_close:
|
||||
canonical_name: "strong_close"
|
||||
type: "boolean_or_null"
|
||||
unit: "none"
|
||||
aliases: ["STRONG_CLOSE"]
|
||||
note: "STRONG_CLOSE_SIGNAL_V1 산출."
|
||||
close_position_pct:
|
||||
canonical_name: "close_position_pct"
|
||||
type: "number_or_null"
|
||||
unit: "percent"
|
||||
aliases: ["CLOSE_POSITION_PCT"]
|
||||
note: "STRONG_CLOSE_SIGNAL_V1 산출 — (close-low)/(high-low)*100."
|
||||
volatility_expansion_breakout:
|
||||
canonical_name: "volatility_expansion_breakout"
|
||||
type: "boolean_or_null"
|
||||
unit: "none"
|
||||
aliases: ["VOLATILITY_EXPANSION_BREAKOUT"]
|
||||
note: "VOLATILITY_EXPANSION_BREAKOUT_V1 산출 — BREAKOUT_QUALITY_GATE_V2 통과 전제."
|
||||
fifty_two_week_high_breakout:
|
||||
canonical_name: "fifty_two_week_high_breakout"
|
||||
type: "boolean_or_null"
|
||||
unit: "none"
|
||||
aliases: ["FIFTY_TWO_WEEK_HIGH_BREAKOUT"]
|
||||
note: "FIFTY_TWO_WEEK_HIGH_TRIGGER_V1 산출 — BREAKOUT_QUALITY_GATE_V2 입력 전용."
|
||||
up_streak:
|
||||
canonical_name: "up_streak"
|
||||
type: "integer_or_null"
|
||||
unit: "count"
|
||||
aliases: ["UP_STREAK"]
|
||||
note: "CONSECUTIVE_STREAK_V1 산출 — 연속 상승 일수."
|
||||
trend_filter_pass:
|
||||
canonical_name: "trend_filter_pass"
|
||||
type: "boolean_or_null"
|
||||
unit: "none"
|
||||
aliases: ["TREND_FILTER_PASS"]
|
||||
note: "TREND_FILTER_GATE_V1 산출 — close>ma120 AND ma120 상승 중."
|
||||
breakout_failure:
|
||||
canonical_name: "breakout_failure"
|
||||
type: "boolean_or_null"
|
||||
unit: "none"
|
||||
aliases: ["BREAKOUT_FAILURE"]
|
||||
note: "BREAKOUT_FAILURE_STOP_V1 산출 — true이면 SELL_RISK_EXIT_REVIEW."
|
||||
|
||||
deployable_cash_contribution_krw:
|
||||
canonical_name: "deployable_cash_contribution_krw"
|
||||
type: "number"
|
||||
|
||||
@@ -112,6 +112,13 @@ formula_registry:
|
||||
- REBALANCE_CADENCE_GATE_V1
|
||||
- WALK_FORWARD_BOOTSTRAP_V1
|
||||
- WEEKLY_LEGACY_TRANSFER_PLAN_V1
|
||||
- GOLDEN_CROSS_SIGNAL_V1
|
||||
- STRONG_CLOSE_SIGNAL_V1
|
||||
- VOLATILITY_EXPANSION_BREAKOUT_V1
|
||||
- FIFTY_TWO_WEEK_HIGH_TRIGGER_V1
|
||||
- CONSECUTIVE_STREAK_V1
|
||||
- BREAKOUT_FAILURE_STOP_V1
|
||||
- TREND_FILTER_GATE_V1
|
||||
implementation_map:
|
||||
REGIME_CONDITIONAL_MACRO_FACTOR_V1: tools/build_predictive_alpha_dialectic_engine_v2.py:NF1
|
||||
REBOUND_CAPTURE_THESIS_FACTOR_V1: tools/build_predictive_alpha_dialectic_engine_v2.py:NF2
|
||||
@@ -151,6 +158,13 @@ formula_registry:
|
||||
REBALANCE_CADENCE_GATE_V1: tools/build_rebalance_cadence_gate_v1.py
|
||||
WALK_FORWARD_BOOTSTRAP_V1: tools/build_walk_forward_bootstrap_v1.py
|
||||
WEEKLY_LEGACY_TRANSFER_PLAN_V1: tools/build_weekly_legacy_transfer_plan_v1.py
|
||||
GOLDEN_CROSS_SIGNAL_V1: tools/build_golden_cross_signal_v1.py
|
||||
STRONG_CLOSE_SIGNAL_V1: tools/build_strong_close_signal_v1.py
|
||||
VOLATILITY_EXPANSION_BREAKOUT_V1: tools/build_volatility_expansion_breakout_v1.py
|
||||
FIFTY_TWO_WEEK_HIGH_TRIGGER_V1: tools/build_fifty_two_week_high_trigger_v1.py
|
||||
CONSECUTIVE_STREAK_V1: tools/build_consecutive_streak_v1.py
|
||||
BREAKOUT_FAILURE_STOP_V1: tools/build_breakout_failure_stop_v1.py
|
||||
TREND_FILTER_GATE_V1: tools/build_trend_filter_gate_v1.py
|
||||
formulas:
|
||||
FLOW_CREDIT_V1:
|
||||
purpose: 가격·거래량·5D 수급 품질을 0~1 점수로 계산
|
||||
@@ -3162,6 +3176,135 @@ formula_registry:
|
||||
canonical_ref: spec/risk/portfolio_exposure.yaml:cash_floor
|
||||
implementation: tools/build_weekly_legacy_transfer_plan_v1.py
|
||||
version: 2026-06-17_P3_v8_9_adoption
|
||||
GOLDEN_CROSS_SIGNAL_V1:
|
||||
purpose: >
|
||||
단기 이동평균(ma20)이 장기 이동평균(ma60)을 상향 돌파하는 골든크로스를 정량 판정한다.
|
||||
STRATEGY_SCORING 보조신호로만 사용 — 단독 BUY 트리거 금지.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-1)
|
||||
inputs:
|
||||
- field: ma20
|
||||
unit: KRW_per_share
|
||||
- field: ma20_prev
|
||||
unit: KRW_per_share
|
||||
- field: ma60
|
||||
unit: KRW_per_share
|
||||
- field: ma60_prev
|
||||
unit: KRW_per_share
|
||||
expression: "golden_cross_today = (ma20_prev <= ma60_prev) AND (ma20 > ma60)"
|
||||
output:
|
||||
field: golden_cross_today
|
||||
unit: boolean
|
||||
missing_policy: ma20_prev/ma60_prev 결측 시 null.
|
||||
canonical_ref: spec/formulas/domains/entry.yaml:GOLDEN_CROSS_SIGNAL_V1
|
||||
implementation: tools/build_golden_cross_signal_v1.py
|
||||
version: 2026-06-18_technical_signals_p4
|
||||
STRONG_CLOSE_SIGNAL_V1:
|
||||
purpose: >
|
||||
종가가 당일 고가-저가 범위 중 고가 근처에서 마감하는지 판정한다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-2)
|
||||
inputs:
|
||||
- field: close_price
|
||||
unit: KRW_per_share
|
||||
- field: high_price
|
||||
unit: KRW_per_share
|
||||
- field: low_price
|
||||
unit: KRW_per_share
|
||||
expression: "close_position_pct = (close_price-low_price)/(high_price-low_price)*100; strong_close = close_position_pct >= 80"
|
||||
output:
|
||||
field: strong_close
|
||||
unit: boolean
|
||||
missing_policy: high_price==low_price면 null.
|
||||
canonical_ref: spec/formulas/domains/entry.yaml:STRONG_CLOSE_SIGNAL_V1
|
||||
implementation: tools/build_strong_close_signal_v1.py
|
||||
version: 2026-06-18_technical_signals_p4
|
||||
VOLATILITY_EXPANSION_BREAKOUT_V1:
|
||||
purpose: >
|
||||
bb_width 수축(squeeze) 후 급등하는 패턴을 판정한다. BREAKOUT_QUALITY_GATE_V2 통과가 전제조건.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-3)
|
||||
inputs:
|
||||
- field: bb_width
|
||||
unit: percent
|
||||
- field: bb_width_20d_percentile
|
||||
unit: percent
|
||||
- field: ret_1d
|
||||
unit: percent
|
||||
expression: "squeeze_detected = bb_width_20d_percentile <= 20; volatility_expansion_breakout = squeeze_detected_previous_day AND ret_1d >= 3.0"
|
||||
output:
|
||||
field: volatility_expansion_breakout
|
||||
unit: boolean
|
||||
missing_policy: bb_width_20d_percentile 결측 시 null.
|
||||
canonical_ref: spec/formulas/domains/entry.yaml:VOLATILITY_EXPANSION_BREAKOUT_V1
|
||||
implementation: tools/build_volatility_expansion_breakout_v1.py
|
||||
version: 2026-06-18_technical_signals_p4
|
||||
FIFTY_TWO_WEEK_HIGH_TRIGGER_V1:
|
||||
purpose: >
|
||||
종가가 52주 최고가(high52w)를 갱신하는지 판정해 BREAKOUT_QUALITY_GATE_V2 입력으로 공급한다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-4)
|
||||
inputs:
|
||||
- field: close_price
|
||||
unit: KRW_per_share
|
||||
- field: high52w
|
||||
unit: KRW_per_share
|
||||
expression: "fifty_two_week_high_breakout = close_price >= high52w"
|
||||
output:
|
||||
field: fifty_two_week_high_breakout
|
||||
unit: boolean
|
||||
missing_policy: high52w 결측 시 null.
|
||||
canonical_ref: spec/formulas/domains/entry.yaml:FIFTY_TWO_WEEK_HIGH_TRIGGER_V1
|
||||
implementation: tools/build_fifty_two_week_high_trigger_v1.py
|
||||
version: 2026-06-18_technical_signals_p4
|
||||
CONSECUTIVE_STREAK_V1:
|
||||
purpose: >
|
||||
N일 연속 상승(up_streak)/하락(down_streak)을 대칭적으로 공식화한다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-5)
|
||||
inputs:
|
||||
- field: daily_close_changes
|
||||
unit: list_of_percent
|
||||
output:
|
||||
field: up_streak
|
||||
unit: count
|
||||
missing_policy: daily_close_changes 비어있으면 null.
|
||||
canonical_ref: spec/formulas/domains/entry.yaml:CONSECUTIVE_STREAK_V1
|
||||
implementation: tools/build_consecutive_streak_v1.py
|
||||
version: 2026-06-18_technical_signals_p4
|
||||
BREAKOUT_FAILURE_STOP_V1:
|
||||
purpose: >
|
||||
전고점 돌파 후 7거래일 이내 재이탈하면 SELL_RISK_EXIT_REVIEW를 발동한다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-6)
|
||||
inputs:
|
||||
- field: prior_high
|
||||
unit: KRW_per_share
|
||||
- field: close_price
|
||||
unit: KRW_per_share
|
||||
- field: days_since_breakout
|
||||
unit: trading_days
|
||||
expression: "breakout_failure = (days_since_breakout <= 7) AND (close_price < prior_high)"
|
||||
output:
|
||||
field: breakout_failure
|
||||
unit: boolean
|
||||
missing_policy: prior_high 결측 시 null.
|
||||
canonical_ref: spec/formulas/domains/exit.yaml:BREAKOUT_FAILURE_STOP_V1
|
||||
implementation: tools/build_breakout_failure_stop_v1.py
|
||||
version: 2026-06-18_technical_signals_p4
|
||||
TREND_FILTER_GATE_V1:
|
||||
purpose: >
|
||||
종가가 ma120 위에 있고 ma120이 상승 중인지 단일 게이트로 판정한다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-7)
|
||||
inputs:
|
||||
- field: close_price
|
||||
unit: KRW_per_share
|
||||
- field: ma120
|
||||
unit: KRW_per_share
|
||||
- field: ma120_prev
|
||||
unit: KRW_per_share
|
||||
expression: "trend_filter_pass = (close_price > ma120) AND (ma120 > ma120_prev)"
|
||||
output:
|
||||
field: trend_filter_pass
|
||||
unit: boolean
|
||||
missing_policy: ma120/ma120_prev 결측 시 null.
|
||||
canonical_ref: spec/strategy/entry_core.yaml:entry_timing_guardrails.regime_based_entry
|
||||
implementation: tools/build_trend_filter_gate_v1.py
|
||||
version: 2026-06-18_technical_signals_p4
|
||||
SELL_EXECUTION_TIMING_V1:
|
||||
purpose: '장중 가격 움직임에 따라 매도 주문 유형과 타이밍을 결정론적으로 판정. 장초반 패닉 매도, 반등 직전 저점 투매 방지.
|
||||
|
||||
|
||||
@@ -254,6 +254,73 @@ golden_cases:
|
||||
input: {weekly_legacy_to_cma_transfer_plan_krw: 4000000, transfer_confirmed: true, transfer_confirmed_amount_krw: 3800000}
|
||||
expected: {deployable_cash_contribution_krw: 3800000.0, plan_status: CONFIRMED_DEPLOYABLE}
|
||||
|
||||
# ── 기술적 신호 7종 채택 (governance/todo/technical_signals_p4_adoption_plan.yaml) ──
|
||||
- formula_id: GOLDEN_CROSS_SIGNAL_V1
|
||||
id: GV4_GCS_001
|
||||
name: ma20가 ma60을 상향 돌파하면 golden_cross_today=true
|
||||
input: {ma20_prev: 98, ma60_prev: 99, ma20: 105, ma60: 100}
|
||||
expected: {golden_cross_today: true}
|
||||
|
||||
- formula_id: GOLDEN_CROSS_SIGNAL_V1
|
||||
id: GV4_GCS_002
|
||||
name: 전일값 결측 시 null(DATA_MISSING), false로 추정 금지
|
||||
input: {ma20_prev: null}
|
||||
expected: {golden_cross_today: null}
|
||||
|
||||
- formula_id: STRONG_CLOSE_SIGNAL_V1
|
||||
id: GV4_SCS_001
|
||||
name: 종가가 고가 근처(90%)면 strong_close=true
|
||||
input: {close: 99, high: 100, low: 90}
|
||||
expected: {strong_close: true, close_position_pct: 90.0}
|
||||
|
||||
- formula_id: STRONG_CLOSE_SIGNAL_V1
|
||||
id: GV4_SCS_002
|
||||
name: high==low(거래정지 등) 시 null
|
||||
input: {high: 100, low: 100}
|
||||
expected: {strong_close: null}
|
||||
|
||||
- formula_id: VOLATILITY_EXPANSION_BREAKOUT_V1
|
||||
id: GV4_VEB_001
|
||||
name: squeeze(저백분위) 후 급등 시 volatility_expansion_breakout=true, BREAKOUT_QUALITY_GATE_V2 별도 통과 필요
|
||||
input: {bb_width_20d_percentile_prev: 10, ret_1d: 4.5}
|
||||
expected: {volatility_expansion_breakout: true, hard_constraint: requires_BREAKOUT_QUALITY_GATE_V2_pass_separately}
|
||||
|
||||
- formula_id: FIFTY_TWO_WEEK_HIGH_TRIGGER_V1
|
||||
id: GV4_FTW_001
|
||||
name: 종가가 52주 최고가 이상이면 breakout=true, 단독 매수 트리거 아님
|
||||
input: {close: 105, high52w: 100}
|
||||
expected: {fifty_two_week_high_breakout: true, hard_constraint: feeds_BREAKOUT_QUALITY_GATE_V2_only_not_buy_trigger}
|
||||
|
||||
- formula_id: CONSECUTIVE_STREAK_V1
|
||||
id: GV4_CST_001
|
||||
name: 최근 3일 연속 상승이면 up_streak=3, down_streak=0
|
||||
input: {daily_close_changes: [1, 2, -1, 1, 2, 3]}
|
||||
expected: {up_streak: 3, down_streak: 0}
|
||||
|
||||
- formula_id: BREAKOUT_FAILURE_STOP_V1
|
||||
id: GV4_BFS_001
|
||||
name: 돌파 후 7일 이내 전고점 아래로 재이탈하면 SELL_RISK_EXIT_REVIEW
|
||||
input: {prior_high: 100, close: 95, days_since_breakout: 3}
|
||||
expected: {breakout_failure: true, gate: SELL_RISK_EXIT_REVIEW}
|
||||
|
||||
- formula_id: BREAKOUT_FAILURE_STOP_V1
|
||||
id: GV4_BFS_002
|
||||
name: 7일 초과 후 재이탈은 breakout_failure 규칙 미적용(false)
|
||||
input: {prior_high: 100, close: 95, days_since_breakout: 10}
|
||||
expected: {breakout_failure: false}
|
||||
|
||||
- formula_id: TREND_FILTER_GATE_V1
|
||||
id: GV4_TFG_001
|
||||
name: 종가가 ma120 위 + ma120 상승 중이면 trend_filter_pass=true
|
||||
input: {close: 105, ma120: 100, ma120_prev: 99}
|
||||
expected: {trend_filter_pass: true}
|
||||
|
||||
- formula_id: TREND_FILTER_GATE_V1
|
||||
id: GV4_TFG_002
|
||||
name: 종가가 ma120 아래면 trend_filter_pass=false
|
||||
input: {close: 95, ma120: 100, ma120_prev: 99}
|
||||
expected: {trend_filter_pass: false}
|
||||
|
||||
# ── STOP_BREACH_V1: profit_pct < -20% 경계값 3 케이스 ─────────────────────
|
||||
- formula_id: STOP_BREACH_V1
|
||||
id: GV4_STOP_001
|
||||
|
||||
@@ -852,3 +852,223 @@ formulas:
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
GOLDEN_CROSS_SIGNAL_V1:
|
||||
purpose: >
|
||||
단기 이동평균(ma20)이 장기 이동평균(ma60)을 상향 돌파하는 골든크로스를 정량 판정한다.
|
||||
독립 BUY 트리거가 아니라 STRATEGY_SCORING의 component_scores 보조신호로만 사용하며,
|
||||
BREAKOUT_QUALITY_GATE_V2/ANTI_LATE_ENTRY_GATE_V2를 우회하지 않는다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-1, 사용자 제시 전략 01_골든크로스)
|
||||
applicable: STRATEGY_SCORING 단계. 단독으로 BUY 의사결정 금지.
|
||||
inputs:
|
||||
- field: ma20
|
||||
unit: KRW_per_share
|
||||
- field: ma20_prev
|
||||
unit: KRW_per_share
|
||||
note: 전일 ma20 값.
|
||||
- field: ma60
|
||||
unit: KRW_per_share
|
||||
- field: ma60_prev
|
||||
unit: KRW_per_share
|
||||
note: 전일 ma60 값.
|
||||
expression: >
|
||||
golden_cross_today = (ma20_prev <= ma60_prev) AND (ma20 > ma60)
|
||||
output:
|
||||
field: golden_cross_today
|
||||
unit: boolean
|
||||
hard_constraint: golden_cross_today=true는 STRATEGY_SCORING 보조점수 가산 입력일 뿐이며 BUY 게이트 체인을 대체하지 않는다.
|
||||
missing_policy: ma20_prev 또는 ma60_prev 결측 시 golden_cross_today=null(DATA_MISSING). false로 추정하지 않는다.
|
||||
canonical_ref: spec/13_formula_registry.yaml:formula_registry.formulas.FLOW_CREDIT_V1
|
||||
implementation: tools/build_golden_cross_signal_v1.py
|
||||
owner: quant_team
|
||||
lifecycle_state: shadow
|
||||
input_fields:
|
||||
- ma20
|
||||
- ma20_prev
|
||||
- ma60
|
||||
- ma60_prev
|
||||
output_fields:
|
||||
- golden_cross_today
|
||||
golden_cases:
|
||||
- golden_cross_basic_detection
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
STRONG_CLOSE_SIGNAL_V1:
|
||||
purpose: >
|
||||
종가가 당일 가격 범위(고가-저가) 중 고가 근처에서 마감하는지(강한 종가)를 정량 판정한다.
|
||||
모멘텀 지속 가능성의 보조신호로만 사용한다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-2, 사용자 제시 전략 07_강한종가)
|
||||
applicable: STRATEGY_SCORING 단계.
|
||||
inputs:
|
||||
- field: close_price
|
||||
unit: KRW_per_share
|
||||
- field: high_price
|
||||
unit: KRW_per_share
|
||||
- field: low_price
|
||||
unit: KRW_per_share
|
||||
expression: >
|
||||
close_position_pct = (close_price - low_price) / (high_price - low_price) * 100 (high==low면 null)
|
||||
strong_close = close_position_pct >= 80
|
||||
output:
|
||||
field: strong_close
|
||||
unit: boolean
|
||||
additional_outputs:
|
||||
- close_position_pct
|
||||
missing_policy: high_price==low_price(거래정지 등)면 close_position_pct=null, strong_close=null.
|
||||
canonical_ref: spec/13_formula_registry.yaml:formula_registry.formulas.FLOW_CREDIT_V1
|
||||
implementation: tools/build_strong_close_signal_v1.py
|
||||
owner: quant_team
|
||||
lifecycle_state: shadow
|
||||
input_fields:
|
||||
- close_price
|
||||
- high_price
|
||||
- low_price
|
||||
output_fields:
|
||||
- strong_close
|
||||
- close_position_pct
|
||||
golden_cases:
|
||||
- strong_close_near_high
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
VOLATILITY_EXPANSION_BREAKOUT_V1:
|
||||
purpose: >
|
||||
변동성이 수축(bb_width 축소)된 뒤 급등(변동성 확장)하는 패턴을 판정한다. 신규 필드 bb_width
|
||||
도입. 이 신호 자체는 BUY를 허가하지 않으며 BREAKOUT_QUALITY_GATE_V2 통과를 전제조건으로 한다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-3, 사용자 제시 전략 08_변동성확장돌파)
|
||||
applicable: STRATEGY_SCORING 단계. BREAKOUT_QUALITY_GATE_V2 PASS 후에만 보조신호로 채택.
|
||||
inputs:
|
||||
- field: bb_width
|
||||
unit: percent
|
||||
note: (상단밴드-하단밴드)/중심선 * 100. 20일 볼린저밴드 기준.
|
||||
- field: bb_width_20d_percentile
|
||||
unit: percent
|
||||
note: 최근 20일 분포 내 현재 bb_width의 백분위. 낮을수록 수축(squeeze) 상태.
|
||||
- field: ret_1d
|
||||
unit: percent
|
||||
expression: >
|
||||
squeeze_detected = bb_width_20d_percentile <= 20
|
||||
volatility_expansion_breakout = squeeze_detected_previous_day AND ret_1d >= 3.0
|
||||
output:
|
||||
field: volatility_expansion_breakout
|
||||
unit: boolean
|
||||
hard_constraint: volatility_expansion_breakout=true이어도 BREAKOUT_QUALITY_GATE_V2 != BLOCKED_LATE_CHASE 조건을 별도로 통과해야 BUY 후보 자격이 생긴다.
|
||||
missing_policy: bb_width 또는 bb_width_20d_percentile 결측 시 squeeze_detected=null. false로 추정하지 않는다.
|
||||
canonical_ref: spec/13_formula_registry.yaml:formula_registry.formulas.BREAKOUT_QUALITY_GATE_V2
|
||||
implementation: tools/build_volatility_expansion_breakout_v1.py
|
||||
owner: quant_team
|
||||
lifecycle_state: shadow
|
||||
input_fields:
|
||||
- bb_width
|
||||
- bb_width_20d_percentile
|
||||
- ret_1d
|
||||
output_fields:
|
||||
- volatility_expansion_breakout
|
||||
golden_cases:
|
||||
- squeeze_then_expansion
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
FIFTY_TWO_WEEK_HIGH_TRIGGER_V1:
|
||||
purpose: >
|
||||
종가가 52주 최고가(High52W)를 갱신하는지 판정해 BREAKOUT_QUALITY_GATE_V2/FOLLOW_THROUGH_DAY_CONFIRM_V1
|
||||
체인의 입력 신호로 공급한다(기존 필드 High52W를 트리거로 명시적으로 연결).
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-4, 사용자 제시 전략 03_52주신고가)
|
||||
applicable: STRATEGY_SCORING 단계. 단독 BUY 트리거 금지 — BREAKOUT_QUALITY_GATE_V2 입력으로만 전달.
|
||||
inputs:
|
||||
- field: close_price
|
||||
unit: KRW_per_share
|
||||
- field: high52w
|
||||
unit: KRW_per_share
|
||||
source: spec/12_field_dictionary.yaml:high52w (alias High52W)
|
||||
expression: >
|
||||
fifty_two_week_high_breakout = close_price >= high52w
|
||||
output:
|
||||
field: fifty_two_week_high_breakout
|
||||
unit: boolean
|
||||
hard_constraint: fifty_two_week_high_breakout=true는 BREAKOUT_QUALITY_GATE_V2 입력으로만 전달되며 그 자체로 BUY를 허가하지 않는다.
|
||||
missing_policy: high52w 결측 시 fifty_two_week_high_breakout=null.
|
||||
canonical_ref: spec/13_formula_registry.yaml:formula_registry.formulas.BREAKOUT_QUALITY_GATE_V2
|
||||
implementation: tools/build_fifty_two_week_high_trigger_v1.py
|
||||
owner: quant_team
|
||||
lifecycle_state: shadow
|
||||
input_fields:
|
||||
- close_price
|
||||
- high52w
|
||||
output_fields:
|
||||
- fifty_two_week_high_breakout
|
||||
golden_cases:
|
||||
- high52w_breakout_detection
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
CONSECUTIVE_STREAK_V1:
|
||||
purpose: >
|
||||
N일 연속 상승(up_streak)/연속 하락(down_streak)을 대칭적으로 공식화한다.
|
||||
down_streak는 REBOUND_CAPTURE_THESIS_FACTOR_V1에 하위조건으로 이미 존재하나 up_streak
|
||||
대응이 없어 비대칭이었다. 이 공식이 단일 출처가 된다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-5, 사용자 제시 전략 04_연속상승하락)
|
||||
applicable: STRATEGY_SCORING 단계, REBOUND_CAPTURE_THESIS_FACTOR_V1 down_streak 입력의 canonical source.
|
||||
inputs:
|
||||
- field: daily_close_changes
|
||||
unit: list_of_percent
|
||||
note: 최근 N거래일의 일별 종가 변화율(%) 리스트. 최신값이 마지막.
|
||||
expression: >
|
||||
up_streak = 마지막 값부터 역순으로 연속 양수(>0)인 일수
|
||||
down_streak = 마지막 값부터 역순으로 연속 음수(<0)인 일수
|
||||
output:
|
||||
field: up_streak
|
||||
unit: count
|
||||
additional_outputs:
|
||||
- down_streak
|
||||
missing_policy: daily_close_changes 비어있으면 up_streak=down_streak=null.
|
||||
canonical_ref: spec/13_formula_registry.yaml:formula_registry.formulas.REBOUND_CAPTURE_THESIS_FACTOR_V1
|
||||
implementation: tools/build_consecutive_streak_v1.py
|
||||
owner: quant_team
|
||||
lifecycle_state: shadow
|
||||
input_fields:
|
||||
- daily_close_changes
|
||||
output_fields:
|
||||
- up_streak
|
||||
- down_streak
|
||||
golden_cases:
|
||||
- up_streak_and_down_streak_symmetry
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
TREND_FILTER_GATE_V1:
|
||||
purpose: >
|
||||
종가가 장기 이동평균(ma120) 위에 있고 ma120 자체가 상승 중인지를 단일 게이트로 공식화한다.
|
||||
entry_core.yaml:regime_based_entry, ANTI_LATE_ENTRY_GATE_V2의 암묵적 추세 조건을 명시적
|
||||
단일 공식으로 통합해 LLM이 추세 판정을 임의로 서술하지 않게 한다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-7, 사용자 제시 전략 10_추세필터)
|
||||
applicable: STRATEGY_SCORING 및 PORTFOLIO_CONSTRAINT_CHECK 입력. 단독으로 BUY 허가하지 않음 — HOLD/AVOID 보조 게이트.
|
||||
inputs:
|
||||
- field: close_price
|
||||
unit: KRW_per_share
|
||||
- field: ma120
|
||||
unit: KRW_per_share
|
||||
- field: ma120_prev
|
||||
unit: KRW_per_share
|
||||
note: 전일 ma120 값. 상승 여부 판정용.
|
||||
expression: >
|
||||
trend_filter_pass = (close_price > ma120) AND (ma120 > ma120_prev)
|
||||
output:
|
||||
field: trend_filter_pass
|
||||
unit: boolean
|
||||
missing_policy: ma120 또는 ma120_prev 결측 시 trend_filter_pass=null(DATA_MISSING).
|
||||
canonical_ref: spec/strategy/entry_core.yaml:entry_timing_guardrails.regime_based_entry
|
||||
implementation: tools/build_trend_filter_gate_v1.py
|
||||
owner: quant_team
|
||||
lifecycle_state: shadow
|
||||
input_fields:
|
||||
- close_price
|
||||
- ma120
|
||||
- ma120_prev
|
||||
output_fields:
|
||||
- trend_filter_pass
|
||||
golden_cases:
|
||||
- trend_filter_above_rising_ma120
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
|
||||
@@ -1544,3 +1544,43 @@ formulas:
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
BREAKOUT_FAILURE_STOP_V1:
|
||||
purpose: >
|
||||
전고점(prior_high)을 돌파한 종목이 며칠 내 다시 그 아래로 이탈하면("돌파 실패") 전용 손절을
|
||||
발동한다. ANTI_WHIPSAW_GATE_V1보다 좁고 구체적인 "돌파 후 재이탈" 패턴 전용 규칙이다.
|
||||
(governance/todo/technical_signals_p4_adoption_plan.yaml P4-6, 사용자 제시 전략 06_돌파실패손절)
|
||||
applicable: EXIT_POLICY_CHECK 단계. 보유 포지션이 돌파 매수로 진입한 경우에만 적용.
|
||||
inputs:
|
||||
- field: prior_high
|
||||
unit: KRW_per_share
|
||||
note: 진입 당시 돌파 기준이 된 전고점.
|
||||
- field: close_price
|
||||
unit: KRW_per_share
|
||||
- field: days_since_breakout
|
||||
unit: trading_days
|
||||
source: spec/13_formula_registry.yaml:formula_registry.formulas.FOLLOW_THROUGH_DAY_CONFIRM_V1
|
||||
expression: >
|
||||
breakout_failure = (days_since_breakout <= 7) AND (close_price < prior_high)
|
||||
output:
|
||||
field: breakout_failure
|
||||
unit: boolean
|
||||
gates:
|
||||
- if: breakout_failure == true
|
||||
action: SELL_RISK_EXIT_REVIEW
|
||||
reason_code: breakout_failure_stop
|
||||
missing_policy: prior_high 또는 days_since_breakout 결측 시 breakout_failure=null. 손절 신호를 임의 추정하지 않는다.
|
||||
canonical_ref: spec/13_formula_registry.yaml:formula_registry.formulas.ANTI_WHIPSAW_GATE_V1
|
||||
implementation: tools/build_breakout_failure_stop_v1.py
|
||||
owner: quant_team
|
||||
lifecycle_state: shadow
|
||||
input_fields:
|
||||
- prior_high
|
||||
- close_price
|
||||
- days_since_breakout
|
||||
output_fields:
|
||||
- breakout_failure
|
||||
golden_cases:
|
||||
- breakout_then_reentry_below_prior_high
|
||||
activation_threshold:
|
||||
min_t20_sample: 30
|
||||
retirement_condition: performance_degradation
|
||||
|
||||
@@ -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_FAILURE_STOP_V1'
|
||||
SCHEMA_ID = 'schema://formula/BREAKOUT_FAILURE_STOP_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/breakout_failure_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,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/BREAKOUT_FAILURE_STOP_V1",
|
||||
"title": "BREAKOUT_FAILURE_STOP_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "BREAKOUT_FAILURE_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": ["prior_high", "close_price", "days_since_breakout"],
|
||||
"x_formula_outputs": ["breakout_failure"]
|
||||
}
|
||||
@@ -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 = 'CONSECUTIVE_STREAK_V1'
|
||||
SCHEMA_ID = 'schema://formula/CONSECUTIVE_STREAK_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/consecutive_streak_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,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/CONSECUTIVE_STREAK_V1",
|
||||
"title": "CONSECUTIVE_STREAK_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "CONSECUTIVE_STREAK_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": ["daily_close_changes"],
|
||||
"x_formula_outputs": ["up_streak", "down_streak"]
|
||||
}
|
||||
@@ -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 = 'FIFTY_TWO_WEEK_HIGH_TRIGGER_V1'
|
||||
SCHEMA_ID = 'schema://formula/FIFTY_TWO_WEEK_HIGH_TRIGGER_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/fifty_two_week_high_trigger_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,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/FIFTY_TWO_WEEK_HIGH_TRIGGER_V1",
|
||||
"title": "FIFTY_TWO_WEEK_HIGH_TRIGGER_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "FIFTY_TWO_WEEK_HIGH_TRIGGER_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", "high52w"],
|
||||
"x_formula_outputs": ["fifty_two_week_high_breakout"]
|
||||
}
|
||||
@@ -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 = 'GOLDEN_CROSS_SIGNAL_V1'
|
||||
SCHEMA_ID = 'schema://formula/GOLDEN_CROSS_SIGNAL_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/golden_cross_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,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/GOLDEN_CROSS_SIGNAL_V1",
|
||||
"title": "GOLDEN_CROSS_SIGNAL_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "GOLDEN_CROSS_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": ["ma20", "ma20_prev", "ma60", "ma60_prev"],
|
||||
"x_formula_outputs": ["golden_cross_today"]
|
||||
}
|
||||
@@ -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 = 'STRONG_CLOSE_SIGNAL_V1'
|
||||
SCHEMA_ID = 'schema://formula/STRONG_CLOSE_SIGNAL_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/strong_close_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,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/STRONG_CLOSE_SIGNAL_V1",
|
||||
"title": "STRONG_CLOSE_SIGNAL_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "STRONG_CLOSE_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": ["close_price", "high_price", "low_price"],
|
||||
"x_formula_outputs": ["strong_close", "close_position_pct"]
|
||||
}
|
||||
@@ -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 = 'TREND_FILTER_GATE_V1'
|
||||
SCHEMA_ID = 'schema://formula/TREND_FILTER_GATE_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/trend_filter_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,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/TREND_FILTER_GATE_V1",
|
||||
"title": "TREND_FILTER_GATE_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "TREND_FILTER_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", "ma120", "ma120_prev"],
|
||||
"x_formula_outputs": ["trend_filter_pass"]
|
||||
}
|
||||
@@ -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 = 'VOLATILITY_EXPANSION_BREAKOUT_V1'
|
||||
SCHEMA_ID = 'schema://formula/VOLATILITY_EXPANSION_BREAKOUT_V1'
|
||||
SCHEMA_PATH = 'schemas/generated/volatility_expansion_breakout_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,16 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "schema://formula/VOLATILITY_EXPANSION_BREAKOUT_V1",
|
||||
"title": "VOLATILITY_EXPANSION_BREAKOUT_V1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formula_id": { "const": "VOLATILITY_EXPANSION_BREAKOUT_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": ["bb_width", "bb_width_20d_percentile", "ret_1d"],
|
||||
"x_formula_outputs": ["volatility_expansion_breakout"]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Golden tests for BREAKOUT_FAILURE_STOP_V1 (governance/todo/technical_signals_p4_adoption_plan.yaml P4-6)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULE_PATH = ROOT / "tools" / "build_breakout_failure_stop_v1.py"
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("build_breakout_failure_stop_v1", MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_reentry_below_prior_high_within_window_triggers_sell_review() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.evaluate(100, 95, 3)
|
||||
assert result["breakout_failure"] is True
|
||||
assert result["gate"] == "SELL_RISK_EXIT_REVIEW"
|
||||
|
||||
|
||||
def test_reentry_after_window_does_not_trigger() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.evaluate(100, 95, 10)
|
||||
assert result["breakout_failure"] is False
|
||||
|
||||
|
||||
def test_missing_prior_high_returns_null() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.evaluate(None, 95, 3)
|
||||
assert result["breakout_failure"] is None
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Golden tests for CONSECUTIVE_STREAK_V1 (governance/todo/technical_signals_p4_adoption_plan.yaml P4-5)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULE_PATH = ROOT / "tools" / "build_consecutive_streak_v1.py"
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("build_consecutive_streak_v1", MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_trailing_three_up_days_gives_up_streak_3() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.compute_streaks([1, 2, -1, 1, 2, 3])
|
||||
assert result["up_streak"] == 3
|
||||
assert result["down_streak"] == 0
|
||||
|
||||
|
||||
def test_trailing_two_down_days_gives_down_streak_2() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.compute_streaks([5, -1, -2])
|
||||
assert result["down_streak"] == 2
|
||||
assert result["up_streak"] == 0
|
||||
|
||||
|
||||
def test_empty_changes_returns_null_not_zero() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.compute_streaks([])
|
||||
assert result["up_streak"] is None
|
||||
assert result["down_streak"] is None
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Golden tests for FIFTY_TWO_WEEK_HIGH_TRIGGER_V1 (governance/todo/technical_signals_p4_adoption_plan.yaml P4-4)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULE_PATH = ROOT / "tools" / "build_fifty_two_week_high_trigger_v1.py"
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("build_fifty_two_week_high_trigger_v1", MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_close_at_or_above_52w_high_triggers() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.evaluate(105, 100) is True
|
||||
|
||||
|
||||
def test_close_below_52w_high_does_not_trigger() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.evaluate(95, 100) is False
|
||||
|
||||
|
||||
def test_missing_high52w_returns_null() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.evaluate(105, None) is None
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Golden tests for GOLDEN_CROSS_SIGNAL_V1 (governance/todo/technical_signals_p4_adoption_plan.yaml P4-1)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULE_PATH = ROOT / "tools" / "build_golden_cross_signal_v1.py"
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("build_golden_cross_signal_v1", MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_ma20_crossing_above_ma60_detected() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.golden_cross_today(105, 98, 100, 99) is True
|
||||
|
||||
|
||||
def test_no_cross_when_ma20_already_above() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.golden_cross_today(105, 101, 100, 99) is False
|
||||
|
||||
|
||||
def test_missing_prev_values_returns_null_not_false() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.golden_cross_today(105, None, 100, 99) is None
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Golden tests for STRONG_CLOSE_SIGNAL_V1 (governance/todo/technical_signals_p4_adoption_plan.yaml P4-2)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULE_PATH = ROOT / "tools" / "build_strong_close_signal_v1.py"
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("build_strong_close_signal_v1", MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_close_near_high_is_strong_close() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.evaluate_strong_close(99, 100, 90)
|
||||
assert result["strong_close"] is True
|
||||
assert result["close_position_pct"] == 90.0
|
||||
|
||||
|
||||
def test_close_near_low_is_not_strong_close() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.evaluate_strong_close(91, 100, 90)
|
||||
assert result["strong_close"] is False
|
||||
|
||||
|
||||
def test_degenerate_high_equals_low_returns_null() -> None:
|
||||
mod = _load_module()
|
||||
result = mod.evaluate_strong_close(100, 100, 100)
|
||||
assert result["strong_close"] is None
|
||||
assert result["close_position_pct"] is None
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Golden tests for TREND_FILTER_GATE_V1 (governance/todo/technical_signals_p4_adoption_plan.yaml P4-7)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULE_PATH = ROOT / "tools" / "build_trend_filter_gate_v1.py"
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("build_trend_filter_gate_v1", MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_above_rising_ma120_passes() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.evaluate(105, 100, 99) is True
|
||||
|
||||
|
||||
def test_below_ma120_fails() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.evaluate(95, 100, 99) is False
|
||||
|
||||
|
||||
def test_above_but_falling_ma120_fails() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.evaluate(105, 100, 101) is False
|
||||
|
||||
|
||||
def test_missing_ma120_returns_null() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.evaluate(105, None, 99) is None
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Golden tests for VOLATILITY_EXPANSION_BREAKOUT_V1 (governance/todo/technical_signals_p4_adoption_plan.yaml P4-3)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
MODULE_PATH = ROOT / "tools" / "build_volatility_expansion_breakout_v1.py"
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("build_volatility_expansion_breakout_v1", MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_squeeze_then_strong_move_triggers_signal() -> None:
|
||||
mod = _load_module()
|
||||
prev_squeeze = mod.squeeze_detected(10)
|
||||
assert prev_squeeze is True
|
||||
assert mod.evaluate(prev_squeeze, 4.5) is True
|
||||
|
||||
|
||||
def test_no_squeeze_does_not_trigger_even_with_strong_move() -> None:
|
||||
mod = _load_module()
|
||||
prev_squeeze = mod.squeeze_detected(80)
|
||||
assert prev_squeeze is False
|
||||
assert mod.evaluate(prev_squeeze, 4.5) is False
|
||||
|
||||
|
||||
def test_missing_percentile_returns_null() -> None:
|
||||
mod = _load_module()
|
||||
assert mod.squeeze_detected(None) is None
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""BREAKOUT_FAILURE_STOP_V1 — spec/formulas/domains/exit.yaml.
|
||||
|
||||
governance/todo/technical_signals_p4_adoption_plan.yaml P4-6.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUT = ROOT / "Temp" / "breakout_failure_stop_v1.json"
|
||||
|
||||
|
||||
def evaluate(prior_high: float | None, close_price: float | None, days_since_breakout: int | None) -> dict:
|
||||
if prior_high is None or close_price is None or days_since_breakout is None:
|
||||
return {"breakout_failure": None}
|
||||
breakout_failure = days_since_breakout <= 7 and close_price < prior_high
|
||||
result = {"breakout_failure": breakout_failure}
|
||||
if breakout_failure:
|
||||
result["gate"] = "SELL_RISK_EXIT_REVIEW"
|
||||
result["reason_code"] = "breakout_failure_stop"
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--prior-high", type=float, default=None)
|
||||
ap.add_argument("--close", type=float, default=None)
|
||||
ap.add_argument("--days-since-breakout", type=int, default=None)
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
|
||||
result = {
|
||||
"formula_id": "BREAKOUT_FAILURE_STOP_V1",
|
||||
**evaluate(args.prior_high, args.close, args.days_since_breakout),
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CONSECUTIVE_STREAK_V1 — spec/formulas/domains/entry.yaml.
|
||||
|
||||
Symmetric up_streak/down_streak from the most recent close-to-close changes.
|
||||
governance/todo/technical_signals_p4_adoption_plan.yaml P4-5.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUT = ROOT / "Temp" / "consecutive_streak_v1.json"
|
||||
|
||||
|
||||
def compute_streaks(daily_close_changes: list[float] | None) -> dict:
|
||||
if not daily_close_changes:
|
||||
return {"up_streak": None, "down_streak": None}
|
||||
|
||||
up_streak = 0
|
||||
for change in reversed(daily_close_changes):
|
||||
if change > 0:
|
||||
up_streak += 1
|
||||
else:
|
||||
break
|
||||
|
||||
down_streak = 0
|
||||
for change in reversed(daily_close_changes):
|
||||
if change < 0:
|
||||
down_streak += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return {"up_streak": up_streak, "down_streak": down_streak}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--changes", default=None, help="comma-separated daily close changes")
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
|
||||
daily_close_changes = [float(x) for x in args.changes.split(",")] if args.changes else None
|
||||
result = {"formula_id": "CONSECUTIVE_STREAK_V1", **compute_streaks(daily_close_changes)}
|
||||
out = Path(args.out)
|
||||
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""FIFTY_TWO_WEEK_HIGH_TRIGGER_V1 — spec/formulas/domains/entry.yaml.
|
||||
|
||||
Feeds BREAKOUT_QUALITY_GATE_V2 -- never an independent BUY trigger.
|
||||
governance/todo/technical_signals_p4_adoption_plan.yaml P4-4.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUT = ROOT / "Temp" / "fifty_two_week_high_trigger_v1.json"
|
||||
|
||||
|
||||
def evaluate(close_price: float | None, high52w: float | None) -> bool | None:
|
||||
if close_price is None or high52w is None:
|
||||
return None
|
||||
return close_price >= high52w
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--close", type=float, default=None)
|
||||
ap.add_argument("--high52w", type=float, default=None)
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
|
||||
result = {
|
||||
"formula_id": "FIFTY_TWO_WEEK_HIGH_TRIGGER_V1",
|
||||
"fifty_two_week_high_breakout": evaluate(args.close, args.high52w),
|
||||
"hard_constraint": "feeds_BREAKOUT_QUALITY_GATE_V2_only_not_buy_trigger",
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""GOLDEN_CROSS_SIGNAL_V1 — spec/formulas/domains/entry.yaml.
|
||||
|
||||
Detects ma20 crossing above ma60 (golden cross). Auxiliary STRATEGY_SCORING signal
|
||||
only -- never an independent BUY trigger. governance/todo/technical_signals_p4_adoption_plan.yaml P4-1.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUT = ROOT / "Temp" / "golden_cross_signal_v1.json"
|
||||
|
||||
|
||||
def golden_cross_today(ma20: float | None, ma20_prev: float | None, ma60: float | None, ma60_prev: float | None) -> bool | None:
|
||||
if None in (ma20, ma20_prev, ma60, ma60_prev):
|
||||
return None
|
||||
return ma20_prev <= ma60_prev and ma20 > ma60
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--ma20", type=float, default=None)
|
||||
ap.add_argument("--ma20-prev", type=float, default=None)
|
||||
ap.add_argument("--ma60", type=float, default=None)
|
||||
ap.add_argument("--ma60-prev", type=float, default=None)
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
|
||||
result = {
|
||||
"formula_id": "GOLDEN_CROSS_SIGNAL_V1",
|
||||
"golden_cross_today": golden_cross_today(args.ma20, args.ma20_prev, args.ma60, args.ma60_prev),
|
||||
"hard_constraint": "auxiliary_signal_only_not_buy_trigger",
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""STRONG_CLOSE_SIGNAL_V1 — spec/formulas/domains/entry.yaml.
|
||||
|
||||
governance/todo/technical_signals_p4_adoption_plan.yaml P4-2.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUT = ROOT / "Temp" / "strong_close_signal_v1.json"
|
||||
|
||||
|
||||
def evaluate_strong_close(close_price: float, high_price: float, low_price: float) -> dict:
|
||||
if high_price == low_price:
|
||||
return {"close_position_pct": None, "strong_close": None}
|
||||
close_position_pct = (close_price - low_price) / (high_price - low_price) * 100
|
||||
return {"close_position_pct": close_position_pct, "strong_close": close_position_pct >= 80}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--close", type=float, required=True)
|
||||
ap.add_argument("--high", type=float, required=True)
|
||||
ap.add_argument("--low", type=float, required=True)
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
|
||||
result = {"formula_id": "STRONG_CLOSE_SIGNAL_V1", **evaluate_strong_close(args.close, args.high, args.low)}
|
||||
out = Path(args.out)
|
||||
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""TREND_FILTER_GATE_V1 — spec/formulas/domains/entry.yaml.
|
||||
|
||||
governance/todo/technical_signals_p4_adoption_plan.yaml P4-7.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUT = ROOT / "Temp" / "trend_filter_gate_v1.json"
|
||||
|
||||
|
||||
def evaluate(close_price: float | None, ma120: float | None, ma120_prev: float | None) -> bool | None:
|
||||
if close_price is None or ma120 is None or ma120_prev is None:
|
||||
return None
|
||||
return close_price > ma120 and ma120 > ma120_prev
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--close", type=float, default=None)
|
||||
ap.add_argument("--ma120", type=float, default=None)
|
||||
ap.add_argument("--ma120-prev", type=float, default=None)
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
|
||||
result = {
|
||||
"formula_id": "TREND_FILTER_GATE_V1",
|
||||
"trend_filter_pass": evaluate(args.close, args.ma120, args.ma120_prev),
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""VOLATILITY_EXPANSION_BREAKOUT_V1 — spec/formulas/domains/entry.yaml.
|
||||
|
||||
This signal does NOT authorize a buy by itself -- callers must separately confirm
|
||||
BREAKOUT_QUALITY_GATE_V2 != BLOCKED_LATE_CHASE. governance/todo/technical_signals_p4_adoption_plan.yaml P4-3.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_OUT = ROOT / "Temp" / "volatility_expansion_breakout_v1.json"
|
||||
|
||||
SQUEEZE_PERCENTILE_THRESHOLD = 20.0
|
||||
EXPANSION_RET_THRESHOLD_PCT = 3.0
|
||||
|
||||
|
||||
def squeeze_detected(bb_width_20d_percentile: float | None) -> bool | None:
|
||||
if bb_width_20d_percentile is None:
|
||||
return None
|
||||
return bb_width_20d_percentile <= SQUEEZE_PERCENTILE_THRESHOLD
|
||||
|
||||
|
||||
def evaluate(squeeze_detected_previous_day: bool | None, ret_1d: float | None) -> bool | None:
|
||||
if squeeze_detected_previous_day is None or ret_1d is None:
|
||||
return None
|
||||
return squeeze_detected_previous_day and ret_1d >= EXPANSION_RET_THRESHOLD_PCT
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bb-width-20d-percentile-prev", type=float, default=None)
|
||||
ap.add_argument("--ret-1d", type=float, default=None)
|
||||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||||
args = ap.parse_args()
|
||||
|
||||
prev_squeeze = squeeze_detected(args.bb_width_20d_percentile_prev)
|
||||
result = {
|
||||
"formula_id": "VOLATILITY_EXPANSION_BREAKOUT_V1",
|
||||
"squeeze_detected_previous_day": prev_squeeze,
|
||||
"volatility_expansion_breakout": evaluate(prev_squeeze, args.ret_1d),
|
||||
"hard_constraint": "requires_BREAKOUT_QUALITY_GATE_V2_pass_separately",
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user