용어 체계: 값=anchoring_value(정수‰)·가격=anchoring_price·조정=adjustment·구간=price_range·표본=sample - DB: rate_adjustments→anchoring.adjustments (id→adjustment_id, price_bracket_index→price_range_index, nego_count→sample_count, anchor_rate_before/after→anchoring_value_before/after, consumed_session_ids→used_session_ids) - sessions: target_anchoring_price→anchoring_price, anchor_rate_permille→anchoring_value, last_offered_price→last_offer_price, anchoring_adjustment_id→used_by_adjustment_id - 뷰: rate_history/current_rates→value_history/current_values, delta_permille→value_change - 코드: calc_price_range_index·calc_anchoring_price·evaluate_samples·get_current_value· get_latest_adjusted_value·get_current_anchoring_value·fetch_current_values·get_base_anchoring_value· Adjustment(ORM)·update_last_offer_price, 상수 ANCHORING_VALUE_MIN/MAX·ADJUSTMENT_STEP· PRICE_RANGE_COUNT/INDEX_MAX, 배치 로그 키 bracket=→price_range= - API: negodata protocol 필드 target_anchoring_price→anchoring_price (front 생성 모델·컴포넌트 동반) - 기존 DB 마이그레이션 신설: schedules/anchoring/migrations/20260706_rename_anchoring.sql (멱등 DO 블록 — 테이블·컬럼·뷰·인덱스·PK 제약. 코드 배포와 동시 적용 필요) - postgres-init 01·04, 문서 6종 동기화 - 실배포 전 수정 포함: main.py argparse 화(--dry-run 단독·오타 플래그 기동 전 차단), 박제 정합식 calc_anchoring_price 재사용, clamped 지표가 실제 포화만 집계(경계값 유지 제외) 주의: sessions.anchoring_value(정수‰)와 quotation_settings.anchoring_value(구 float 비율)는 같은 이름·다른 단위 — 구 컬럼은 미변경. 검증: 모듈 20·negodata 50·backend 57 테스트 통과, front tsc·vite build 통과, 로컬 DB 마이그레이션 적용 후 배치 dry-run·상주 기동·양 서버 부팅 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
60 lines
2.5 KiB
Python
60 lines
2.5 KiB
Python
"""정적 기본 테이블 — 칸 시작값의 유일한 소스. 원본: schedules/anchoring/src/anchoring/base_table.py
|
|
|
|
resources/anchoring_base.json(46행 사다리, 불변)을 최초 사용 시 메모리에 로드한다.
|
|
DB 에 저장하지 않으며 런타임에 절대 수정하지 않는다. 검증 실패 시 예외(견적 생성 차단이 아니라
|
|
잘못된 리소스 배포를 조기에 드러내기 위함 — 파일은 코드와 함께 배포되므로 정상 배포에선 실패하지 않는다).
|
|
"""
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from common.anchoring.constants import PRICE_RANGE_COUNT, UPPER_BOUNDS
|
|
|
|
_RESOURCE = Path(__file__).parent / "resources" / "anchoring_base.json"
|
|
|
|
_rates: list[int] | None = None # price_range_index → 시작값(‰)
|
|
|
|
|
|
class BaseTableError(RuntimeError):
|
|
"""정적 테이블 로드/검증 실패."""
|
|
|
|
|
|
def _validate(rows: list) -> list[int]:
|
|
"""행 검증 후 천분율 정수 리스트로 변환. 실패 시 BaseTableError.
|
|
|
|
규약: 46행 · idx 1..46 연속 · upper_bound == 사다리(UPPER_BOUNDS) · 값 0.01~0.20.
|
|
"""
|
|
if not isinstance(rows, list) or len(rows) != PRICE_RANGE_COUNT:
|
|
raise BaseTableError(f"정적 테이블 행 수 불일치: {len(rows) if isinstance(rows, list) else type(rows)} != {PRICE_RANGE_COUNT}")
|
|
rates: list[int] = []
|
|
for i, row in enumerate(rows):
|
|
idx = row.get("idx")
|
|
ub = row.get("upper_bound")
|
|
av = row.get("anchoring_value")
|
|
if idx != i + 1:
|
|
raise BaseTableError(f"idx 불연속: 위치 {i} 의 idx={idx} (기대 {i + 1})")
|
|
if ub != UPPER_BOUNDS[i]:
|
|
raise BaseTableError(f"upper_bound 사다리 불일치: idx={idx} upper_bound={ub} (기대 {UPPER_BOUNDS[i]})")
|
|
if not isinstance(av, (int, float)) or av != av or not (0.01 <= av <= 0.20):
|
|
raise BaseTableError(f"anchoring_value 범위 밖: idx={idx} value={av}")
|
|
rates.append(int(round(av * 1000)))
|
|
return rates
|
|
|
|
|
|
def load_base_table() -> None:
|
|
"""리소스 파일 로드 + 검증. 최초 1회 호출(멱등)."""
|
|
global _rates
|
|
if _rates is not None:
|
|
return
|
|
try:
|
|
rows = json.loads(_RESOURCE.read_text())
|
|
except Exception as ex:
|
|
raise BaseTableError(f"정적 테이블 파일 로드 실패: {_RESOURCE}: {ex}") from ex
|
|
_rates = _validate(rows)
|
|
|
|
|
|
def get_base_anchoring_value(price_range_index: int) -> int:
|
|
"""구간 인덱스 → 시작 앵커링 값(‰)."""
|
|
if _rates is None:
|
|
load_base_table()
|
|
return _rates[price_range_index]
|