o2o-negosium-original/schedules/anchoring/README.md
민헌 f29e9723b0 feat(anchoring): 앵커링 값 자동 조정 자립 모듈 신설 (v1.2)
schedules/anchoring — backend 를 import 하지 않는 독립 컨테이너 배치 서비스.
회사 × 협력사유형(1유통/2제조/3총판) × 가격구간(3,000원, 33,334칸)별 앵커링
값(정수 천분율)을 격주 토 00:00 KST 에 협상 성공률로 자동 조정한다.

- 판정 = "가격 흔적" 기준: last_offered_price 가 있는 종료 재협상만 표본,
  DONE & bid ≤ 박제 앵커만 성공, 나머지(초과 합의·결렬·가격 쓰고 이탈) 실패.
  앵커는 비노출(엔진 내부 체결 임계) — agent 무변경
- 저장 = anchoring.rate_adjustments 1개(append-only, consumed_session_ids 박제),
  소비 경계 = sessions.anchoring_adjustment_id 마킹(멱등·이월). DDL 은 모듈
  소유(schema.sql, sessions 3컬럼 ALTER 포함)
- 안정성: 조정 INSERT+마킹 한 트랜잭션 + rowcount 불일치 전체 롤백,
  Redis TTL 7일 + 매주 조정 칸 re-SET, socket timeout 0.3s, DB 폴백,
  가격 제시율 0% WARN, --once 수동 캐치업
- 정적 기본 테이블(전 구간 10‰, 상한 정확히 1억·초과분 마지막 인덱스 클램프)
  기동 검증 실패 시 기동 중단
- 전체 async(SQLAlchemy+asyncpg, redis.asyncio) — negodata 가 reader 를 그대로
  이식 가능(docs/인수인계.md). 최종 문서 docs/{개발용,기획용,워크플로우}.md
- 테스트 15종: 골든 벡터(§11) + DB 통합(멱등·이월·격리·rowcount 롤백)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:29:46 +09:00

3.5 KiB
Raw Blame History

anchoring — 앵커링 값 자동 조정 배치 (자립 모듈)

회사 × 협력사유형(1유통/2제조/3총판) × 가격구간(3,000원, 33,334칸)별 앵커링 값(‰)을 격주 토 00:00 KST 배치로 협상 성공률에 따라 자동 조정한다. schedules/anchoring 아래에서 완전 독립으로 동작 — backend 코드를 import 하지 않는다.

규범 문서: docs/개발용.md (정책: docs/기획용.md, 흐름 해설: docs/워크플로우.md, 타 팀 적용: docs/인수인계.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

운영 런북

  • 미스파이어: 토 00:00 에 서비스가 내려가 있었고 1시간(misfire_grace) 초과로 그 회차가 스킵됐다면, 재기동 후 --once 1회 실행으로 즉시 캐치업(격주 게이트만 무시, 정책 파라미터 불변).
  • Redis 유실/재기동: 캐시는 파생값 — 매 실행(매주, 게이트 무관) 시작 시 조정 보유 칸 전체를 re-SET 하고 TTL 7일이 보조하므로 자가 회복된다. 수동 복구가 필요하면 --once.
  • 노출률 0% WARN: agent 스크립트 스텝명(기존가격제시) 변경이나 backend 노출 기록 배선 유실 신호 — 즉시 점검.
  • 조정 이력은 append-only — UPDATE/DELETE 금지. 배치가 sessions 에 쓰는 컬럼은 anchoring_adjustment_id 하나뿐.