균일 3,000원 × 33,334칸 → 자릿수 사다리 46칸. 폭 = 구간 상한의 10% (선행 자릿수 밴드): [0, 1,000) 통일 1칸 + 자릿수(1천~1억, 5개)당 9칸 — "1천 원대·2천 원대 … 9천만 원대". 1억 초과는 마지막 인덱스(45) 클램프. - 고가 구간의 무의미한 3,000원 해상도 제거 → 칸당 표본 밀도 대폭 개선 (희소 칸 이월 문제 구조적 완화), 정적 테이블 2MB → 3KB - 사다리 단일 소스 = constants.UPPER_BOUNDS(생성식), json 은 기동 시 대조 검증 - calc_bracket_index: // 3000 → bisect_right (좌폐우개 경계 규약 동일 유지) - 조정 이력 0건 시점 적용 — 인덱스 재매핑/마이그레이션 없음. DB 스키마 무변경 - 골든 테스트 재작성(경계·자릿수 진입·클램프) + 사다리 형태 검증 추가 — 16 passed - 문서 5종·인수인계·README·스키마 주석 동기화, TODO 과제 1 완료 처리 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.5 KiB
4.5 KiB
anchoring — 앵커링 값 자동 조정 배치 (자립 모듈)
회사 × 협력사유형(1유통/2제조/3총판) × 가격구간(자릿수 계단식 사다리, 46칸 — 예: 3만 원대)별 앵커링 값(‰)을
격주 토 00:00 KST 배치로 협상 성공률에 따라 자동 조정한다.
schedules/anchoring 아래에서 완전 독립으로 동작 — backend 코드를 import 하지 않는다.
규범 문서:
docs/개발용.md(정책:docs/기획용.md, 흐름 해설:docs/워크플로우.md, 타 팀 적용:docs/인수인계.md) 처음 오신 분 / 운영 담당자 →docs/운영및유지보수.md부터 보세요 (설치·실행·로그 읽기·트러블슈팅). 후속 과제(가격구간 계단식 재설계, anchoring_records 조회 테이블) →TODO.md
경계
| 구분 | 대상 |
|---|---|
| 소유(쓰기) | anchoring.rate_adjustments(append-only 조정 이력), sessions.anchoring_adjustment_id(소비 마킹 — 이 컬럼만), Redis anchor:* 키 |
| 읽기 전용 | negotiation.sessions(박제 컬럼), quotation.quotations.supplier_type, partner.items.company_id |
| 소비자 | negodata 가 reader.get_anchor_rate 이식 + 이 Redis 를 참조해 세션 생성 시 앵커가 박제 (인수인계) |
구조
schema.sql # 모듈 소유 DDL (rate_adjustments + sessions 3컬럼) — psql 수동 적용
src/anchoring/
constants.py # 상수·enum (δ={1:20, 2:10, 3:15} — 제조/총판 스왑 주의)
resources/anchoring_base.json # 정적 기본 테이블(33,334칸, 전부 10‰) — 불변, 시작값의 유일한 소스
base_table.py # 로드+검증(실패 시 기동 중단)
service.py # 순수 계산 (구간·앵커가·판정·평가) — negodata 이식 대상
reader.py # 현재 rate 조회: Redis → 조정 이력 → 정적 테이블 — negodata 이식 대상
redis_client.py # TTL 7일, socket timeout 0.3s, 장애 시 DB 폴백
batch.py # 격주 평가: 캐시 re-SET → 스캔·파생 판정 → 조정 INSERT+마킹(한 트랜잭션, rowcount 롤백)
scheduler.py # 매주 토 00:00 트리거 (격주 게이트는 잡 내부 ISO 주차 홀짝)
main.py # 엔트리 (상주 / --once)
tests/ # 골든 벡터(test_core) + DB 통합(test_batch — 로컬 Postgres 없으면 자동 스킵)
실행
# 0) DDL 적용 (신규 DB: postgres-init/01~04 이후)
psql -h 127.0.0.1 -U postgres -d negosium_db -f schema.sql
# 로컬(가상환경)
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp config.toml.example config.toml # DB/Redis 채우기 (env 로 대체 가능)
PYTHONPATH=src .venv/bin/python -m anchoring.main --once # 수동 1회(격주 게이트 무시)
PYTHONPATH=src .venv/bin/python -m anchoring.main # 스케줄러 상주
# 도커(자립 compose: redis 동봉)
docker compose up -d --build
# 테스트
PYTHONPATH=src .venv/bin/python -m pytest tests/ -q
로그 확인
docker logs anchoring | grep "batch 20260705" # 특정 회차 전체
docker logs anchoring | grep "company=<uuid>" # 특정 회사만 (조정·회사요약 라인)
docker logs anchoring | grep -E "WARNING|ERROR" # 이상 신호만
- 타임스탬프는 항상 KST. 회차마다
조정 company=... n=13 성공=8 10‰→30‰ adj_id=26(칸별 상세)과회사요약 company=...(테넌트별 집계) 라인이 남고,adj_id로anchoring.rate_adjustments행과 교차 확인한다. - 칸 실패가 있으면 종료 요약이 WARNING 으로 승격된다 — "WARN 이상 알람" 룰에 걸린다.
- 로그 로테이션은 compose 에 설정됨(10MB × 5). 영구 감사 추적은 로그가 아니라 DB(조정 이력 ↔ 세션 마킹)가 담당.
운영 런북
- 미스파이어: 토 00:00 에 서비스가 내려가 있었고 1시간(misfire_grace) 초과로 그 회차가 스킵됐다면,
재기동 후
--once1회 실행으로 즉시 캐치업(격주 게이트만 무시, 정책 파라미터 불변). - Redis 유실/재기동: 캐시는 파생값 — 매 실행(매주, 게이트 무관) 시작 시 조정 보유 칸 전체를 re-SET 하고
TTL 7일이 보조하므로 자가 회복된다. 수동 복구가 필요하면
--once. - 노출률 0% WARN: agent 스크립트 스텝명(
기존가격제시) 변경이나 backend 노출 기록 배선 유실 신호 — 즉시 점검. - 조정 이력은 append-only — UPDATE/DELETE 금지. 배치가 sessions 에 쓰는 컬럼은
anchoring_adjustment_id하나뿐.