#!/usr/bin/env bash
# 07편 — orallexa(34) 의 「안 낸 결정」을 반사실 시험 주문으로 채점한다.
#
#   ┌─ 설계 원칙 ────────────────────────────────────────────────────────────┐
#   │ **의도는 도구가 갖는다.** weltiq 는 집행 기록(주문·체결·수수료·청산 사유)만 갖고,   │
#   │ p_up·판정·게이트 통과 여부는 **저장하지 않는다.** 아래 심기 루프를 보라 —         │
#   │ 서버로 가는 것은 client_order_id · client_tracking_id · (문자열) metadata 뿐이고, │
#   │ verdict·gate·화면확신도는 tool_log.csv 에만 쓴다.                            │
#   │ 나중에 둘을 잇는 것은 **추적 ID 하나**다.                                      │
#   └───────────────────────────────────────────────────────────────────────┘
#
# 이 레시피가 채우는 빈칸:
#   34 의 Brier 감사는 `probabilities.up` 을 채점하는데, `WAIT` 행은 **방향 주장이 없어**
#   채점 대상에서 빠진다 (RUN-LOG:214 — 20행 중 WAIT 11행). 빠진 것은 확률이 아니라 **방향**이다.
#   반사실 시험 주문이 그 방향 하나를 공급하면 그 11행이 채점 가능해진다.
#   그리고 게이트에 막힌 BUY(`SKIP MSFT — confidence 50.8% below gate 60.0%`)는
#   34 쪽에서는 이미 채점 대상이다 — 34 는 주문이 나갔는지를 보지 않는다.
#   즉 이 레시피가 새로 여는 것은 WAIT 쪽이고, 게이트 쪽은 `actual` 을 더 빨리 주는 경로다.
#
# 쓰는 법 (pro 계정 API 키만 있으면 된다):
#
#   export WELTIQ_API_KEY='wk_...'        # scope: run-forward-tests, read-data
#   export RUN_ID='orallexa-cf-1'         # 이번 런의 추적 ID — 아무 문자열이나
#   ./07-trial-order-orallexa-calibration.sh
#
# 산출물은 ./cf-07-$RUN_ID/ 에 떨어진다:
#   tool_log.csv  — 도구가 가진 판정·p_up·게이트 (서버는 모른다)
#   all.csv       — 계정 전체 집행 기록 (한 번에 받는다)
#   joined.csv    — 추적 ID + client_order_id 로 둘을 조인한 것 ← 목적지
#
# 주의: 시험 주문은 **월 한도**를 쓴다 (진입 접수 1건 = 1회). 아래 PLAN 은 3건이다.
#       **실행하지 않았다.** 이 파일은 `bash -n` 까지만 확인했다.
set -euo pipefail

: "${WELTIQ_BASE_URL:=https://weltiq.ai}"
: "${WELTIQ_API_KEY:?WELTIQ_API_KEY 를 설정하세요 (pro 계정 API 키, scope: run-forward-tests read-data)}"
: "${RUN_ID:=orallexa-cf-$(date +%Y%m%d-%H%M%S)}"
WORK="${WORK_DIR:-./cf-07-$RUN_ID}"

command -v jq >/dev/null || { echo 'jq 가 필요합니다 (brew install jq)'; exit 2; }
mkdir -p "$WORK"
CALLS=0
api() { CALLS=$((CALLS + 1)); curl -sS -H "Authorization: Bearer $WELTIQ_API_KEY" \
        -H 'Content-Type: application/json' "$@"; }

# ── ⓪ 틱 반올림 — 왜 원값이 422 인가 ────────────────────────────────────────
# 34 의 BTC-USD 레벨 원값은 `risk_module.stop_loss_price = 82034.9994` 다 (raw/btc-decision.json).
# binance-spot BTCUSDT 의 틱은 0.01 이고 82034.9994 는 그 배수가 아니다
#   → `422` (통제군으로 그대로 보내 확인했다: 케이스 P34-BTC-RAW).
# 서버는 **조용히 반올림해 주지 않는다** — 조건 가격도 실제 주문과 같은 격자에 있어야 하고,
# 사용자가 준 가격을 서버가 몰래 바꾸는 쪽이 더 위험하다. 부르는 쪽에서 맞춘다.
# 방향을 적어 둔다: 롱 기준으로 손절은 **올림**, 목표는 **버림** — 양쪽 다 보수적이다.
STOP_RAW='82034.9994';   STOP='82035.00'      # 올림  (손절을 가깝게)
TP_RAW='88345.384';      TP='88345.38'        # 버림  (목표를 가깝게)
echo "   틱 보정: stop $STOP_RAW → $STOP · target $TP_RAW → $TP (틱 0.01)"

# ── ① 심기 — 주문은 서버로, p_up 은 tool_log.csv 로 ──────────────────────────
# 한 줄 = client_order_id | venue | symbol | verdict | p_up | gate | stop | tp | 청산까지 몇 초
#
# 세 건의 출처 (`reports/a/34-orallexa/raw/probe-basket.json` · `raw/pilot-gate60.txt`):
#   - NVDA  WAIT p_up 0.40 — 레벨이 없다. 지어내지 않고 **시한만** 준다.
#   - MSFT  BUY  p_up 0.55 — 판단은 BUY 인데 게이트 60% 가 막았다 (`SKIP … below gate 60.0%`).
#                            실주문 로그가 `stop=$0.00, target=$0.00` 이라 레벨이 없다 → 시한만.
#   - BTC   BUY  p_up 0.55 — 게이트가 막았고, 이쪽은 도구 자신의 손절·목표가 있다 → 그대로 건다.
#
# **방향을 왜 롱으로 골랐나**: 세 건 다 `probabilities.up > probabilities.down` 이다.
# 하락을 시사한 WAIT 도 있지만(META 0.25/0.45 등) **34 기록 어디에도 그 종목의 가격이 없어**
# "그 도구 기록의 가격대 근처" 규칙을 지킬 수 없다 → 이 레시피에 안 넣는다.
PLAN='
'"$RUN_ID"'-nvda|us-equity|NVDA|WAIT|0.40|passthrough|||600
'"$RUN_ID"'-msft|us-equity|MSFT|BUY|0.55|blocked-60||| 600
'"$RUN_ID"'-btc|binance-spot|BTCUSDT|BUY|0.55|blocked-60|'"$STOP"'|'"$TP"'|600
'

echo 'client_order_id,tracking,verdict,p_up,gate,symbol' > "$WORK/tool_log.csv"
echo "① 심기 — tracking=$RUN_ID"
while IFS='|' read -r COID VENUE SYM VERDICT PUP GATE SL TP_ CLOSE_S; do
  [ -n "${COID:-}" ] || continue
  # 청산 계획: 레벨이 있으면 같이 걸고, 없으면 **시한만** 준다. 없는 값을 지어내지 않는다.
  # 시한은 필수 필드다 — 빼면 `422 exit_plan_deadline_required` 다.
  PLANJ=$(jq -n --argjson sec "$(echo "$CLOSE_S" | tr -d ' ')" --arg sl "$SL" --arg tp "$TP_" \
    '{close_after_seconds:$sec}
     + (if $sl == "" then {} else {stop_loss:$sl} end)
     + (if $tp == "" then {} else {take_profit:$tp} end)')
  # ↓ 서버로 가는 본문.
  #   metadata 에 **float 을 넣으면 500** 이다 — canonical_json 이 결정론 규칙으로 float 을 거부한다
  #   (weltiq/core/canonical.py:18 「float 금지 — Decimal 또는 str 을 사용 (결정론)」).
  #   그래서 p_up 은 **문자열**로 싣는다: {"p_up": "0.55"}.
  #   그리고 어차피 채점은 우리 쪽이 하지 않는다 — 아래 ④ 를 보라.
  BODY=$(jq -n --arg v "$VENUE" --arg s "$SYM" --arg c "$COID" --arg t "$RUN_ID" \
               --arg pu "$PUP" --arg vd "$VERDICT" --arg g "$GATE" --argjson ep "$PLANJ" \
    '{venue:$v, symbol:$s, client_order_id:$c, side:"long",
      client_tracking_id:$t, tags:["counterfactual","a2-07"],
      exit_plan:$ep, max_duration_hours:1,
      metadata:{p_up:$pu, verdict:$vd, gate:$g, tool:"34-orallexa"}}')
  RESP=$(api -X POST "$WELTIQ_BASE_URL/v1/trial-buys" -d "$BODY")
  echo "   $COID → $(echo "$RESP" | jq -r '.test_id // .error // "?"')"
  # ↓ 도구 쪽 로그. **서버에는 안 보낸다** — 게이트·화면확신도는 34 의 것이다.
  printf '%s,%s,%s,%s,%s,%s\n' "$COID" "$RUN_ID" "$VERDICT" "$PUP" "$GATE" "$SYM" >> "$WORK/tool_log.csv"
done <<< "$PLAN"

echo "   (체결·청산이 돌 시간을 준다: 660초)"; sleep "${SETTLE_S:-660}"

# ── ② 회수 — tracking= 으로 계정 전체에서 내 런만 ────────────────────────────
# `tracking=` 은 정확 일치 필터다. 내 계정 안에서만 돈다 — 다른 런은 섞이지 않는다.
echo "② 회수 — tracking=$RUN_ID"
api "$WELTIQ_BASE_URL/v1/forward-tests?tracking=$RUN_ID&limit=200" > "$WORK/list.json"
jq -r '.tests[].test_id' "$WORK/list.json" > "$WORK/test_ids.txt"
echo "   시험 $(wc -l < "$WORK/test_ids.txt" | tr -d ' ')건"

# ── ③ 집행 기록 — 계정 전체 CSV 를 **한 번에** ───────────────────────────────
# 시험마다 export 를 부르면 「목록 1 + 시험 N」이다. 이 엔드포인트는 필터에 걸린 전부를
# 커서로 끝까지 돌아 **한 번에** 낸다. 열은 시험 하나 내보내기와 글자까지 같다.
# 자동 청산 행도 같이 온다 — 진입의 client_tracking_id·tags 를 물려받기 때문이다.
# 실현 손익은 **그 청산 행에 붙는다.** 이게 없으면 회수가 진입만 가져가고 손익을 빼놓는다.
echo '③ 집행 기록 — 계정 전체 CSV 한 번에'
api "$WELTIQ_BASE_URL/v1/forward-tests/orders?format=csv&tracking=$RUN_ID" > "$WORK/all.csv"
echo "   $(($(wc -l < "$WORK/all.csv") - 1))행"

# ── ④ 조인 + 반사실 채점 ────────────────────────────────────────────────────
# CSV 에 인용된 쉼표(metadata·exit_plan)가 있어 awk 로 자르면 깨진다. 조인은 python3 csv 모듈로.
echo '④ 조인 — all.csv × tool_log.csv, 그리고 반사실 채점'
python3 - "$WORK" <<'PY'
import csv, sys
w = sys.argv[1]
log = {(r['tracking'], r['client_order_id']): r for r in csv.DictReader(open(f'{w}/tool_log.csv'))}
rows = list(csv.DictReader(open(f'{w}/all.csv')))
cols = (list(rows[0]) if rows else []) + ['verdict', 'p_up', 'gate', 'join']
out = csv.DictWriter(open(f'{w}/joined.csv', 'w', newline=''), cols); out.writeheader()
per, matched = {}, 0
for r in rows:
    m = log.get((r['client_tracking_id'], r['client_order_id']))
    r.update(verdict=m['verdict'] if m else '', p_up=m['p_up'] if m else '',
             gate=m['gate'] if m else '', join='matched' if m else 'unmatched')
    matched += bool(m)
    out.writerow(r)
    t = per.setdefault(r['test_id'], {'p_up': None, 'verdict': '', 'sym': '',
                                      'enter': None, 'exit': None, 'reason': ''})
    if m:
        t.update(p_up=float(m['p_up']), verdict=m['verdict'], sym=m['symbol'])
    if r.get('close_reason'):
        t['reason'] = r['close_reason']
    if r['fill_qty']:
        t['enter' if r['action'] == 'enter' else 'exit'] = (
            float(r['fill_qty']), float(r['fill_px']), float(r['fee'] or 0))
print(f'   조인됨 {matched} / 안 됨 {len(rows) - matched}')
# 자동 청산 행은 진입의 추적 ID 를 물려받지만 client_order_id 는 "exit-plan:…" 이라
# tool_log 에 있을 수 없다. 그 행이 unmatched 로 나오는 것은 정상이다.

# 반사실 채점 — Brier 의 `actual` 을 순손익 부호로 채운다.
# 34 의 brier_audit.py:301 은 forecast_p 로 `probabilities.up` 을 쓴다. 우리는 그 값을 그대로 두고
# **actual 하나만** 공급한다. 채점은 34 쪽에서 돌린다 — 우리는 사실과 계산만 낸다.
print(f'\n   {"종목":<8}{"판정":<6}{"p_up":>6}{"청산사유":>20}{"순손익":>16}{"손익분기%":>11}{"actual":>8}')
for t in per.values():
    if t['p_up'] is None:
        continue
    if not (t['enter'] and t['exit']):
        # 체결이 없으면 판정할 것이 없다. **미판정은 미판정이라고 적는다.**
        # us-equity 는 폐장이면 접수(201) 뒤 close_reason=rejected:market_closed 로 돌아온다.
        print(f'   {t["sym"]:<8}{t["verdict"]:<6}{t["p_up"]:>6}{t["reason"] or "미체결":>20}'
              f'{"미판정":>16}{"—":>11}{"—":>8}')
        continue
    fee = t['enter'][2] + t['exit'][2]
    notional = t['enter'][0] * t['enter'][1]
    net = t['exit'][0] * t['exit'][1] - notional - fee
    # 손익분기 거리 = |순손익| / 진입 금액.
    # 고정 시장에서 이 값이 곧 「그 판단이 값어치를 내려면 필요했던 최소 움직임」이다.
    print(f'   {t["sym"]:<8}{t["verdict"]:<6}{t["p_up"]:>6}{t["reason"]:>20}'
          f'{net:>16.10f}{abs(net) / notional * 100:>11.4f}{1 if net > 0 else 0:>8}')
print('   (actual = 순손익 부호. 34 의 forecast_p 는 손대지 않는다 — 우리가 공급하는 것은 방향뿐이다.)')
print('   (미판정 행을 0 으로 채우지 마라. 안 잰 것과 0 은 다르다.)')
PY

echo
echo "끝. HTTP 호출 $CALLS 회 · 산출물 $WORK/{tool_log,all,joined}.csv"
echo "   회수는 목록 1 + 전체 CSV 1 = 2회다 (시험 수와 무관)."
