#!/usr/bin/env bash
# weltiq 시험 주문 — 11편: **같은 시각·같은 호가에 진입가 한 칸만 바꿔** 두 팔을 함께 낸다.
#
#   ┌─ 이 레시피가 재는 것 ───────────────────────────────────────────────────┐
#   │ QuantDinger 격자가 고른 두 최적점(60/180 · 120/360)이 **우리 쪽에서 실제로 낸**  │
#   │ 진입가 두 개를, 같은 국면 호가에 나란히 건다. 바뀌는 칸은 `limit_price` 하나다.    │
#   │                                                                        │
#   │ **격자 파라미터(fast/slow) 자체는 이 레시피로 대조할 수 없다.** 시험 주문 본문에    │
#   │ 기간 칸이 없고, 도구의 격자 행에 거래별 레벨이 없다. 그 이야기가 11편 본문이다.     │
#   └───────────────────────────────────────────────────────────────────────┘
#
#   ┌─ 설계 원칙 — 의도는 도구가 갖는다 ──────────────────────────────────────┐
#   │ weltiq 는 집행 기록(주문·체결·수수료·청산 사유)을 갖는다. **왜 그 진입가를 골랐는지**  │
#   │ (격자 순위·PF·거래 수·어떤 비용 가정에서 나왔는지)는 **서버에 보내지 않는다.**        │
#   │ 아래 ① 을 보라 — 서버로 가는 것은 client_order_id · client_tracking_id ·        │
#   │ metadata.arm 뿐이고, grid·pf·trades 는 tool_log.csv 에만 쓴다.                │
#   │ 나중에 둘을 잇는 것은 **추적 ID 하나**다.                                      │
#   └───────────────────────────────────────────────────────────────────────┘
#
# 쓰는 법 (pro 계정 API 키만 있으면 된다):
#
#   export WELTIQ_API_KEY='wk_...'          # scope: run-forward-tests, read-data
#   export RUN_ID='qd-grid-ab-1'            # 이번 대조의 추적 ID — 두 팔이 같은 값을 쓴다
#   export BASE_PX=85979.70 VAR_PX=85795.14 STOP_PX=81605.00
#   ./11-trial-order-quantdinger-grid-ab.sh
#
# 산출물은 ./ab-$RUN_ID/ 에 떨어진다:
#   tool_log.csv  — 도구가 가진 격자 근거 (서버는 모른다)
#   all.csv       — 계정 전체 주문 CSV 를 추적 ID 로 걸러 한 번에 받은 것
#   joined.csv    — 추적 ID + client_order_id 로 둘을 조인한 것 ← 목적지
#
# 주의: **대조군 N개 = 월 한도 N회**다. 아래 PLAN 은 2건이다.
#       청산·재전송(같은 본문)·409·조회·내보내기는 0회다.
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:=qd-grid-ab-$(date +%Y%m%d-%H%M%S)}"
: "${VENUE:=binance-spot}"
: "${SYMBOL:=BTCUSDT}"
: "${CLOSE_S:=45}"                 # 관측 창. **첫 진입 체결**부터의 초다 (접수부터가 아니다)
: "${MAX_H:=1}"                    # 수명 상한. 미체결 팔은 결국 여기서 취소된다

# ── 틱 반올림 — 원래 값을 그대로 보내면 422 다 ────────────────────────────────
# 우리 기록의 실제 진입 체결가는 85979.70711050 / 85795.14865650 이다. 그대로 `limit_price` 에
# 넣으면 `422 invalid_limit_price "px 85979.70711050 is not a multiple of tick 0.01"` —
# 조건 가격도 실제 주문과 **같은 격자**에 있어야 하고, 서버가 사용자 가격을 몰래 바꾸지 않는다.
# 그래서 호출하는 쪽에서 버린다. **롱 지정가는 내리는 쪽이 보수적**이므로 올림이 아니라 버림이다.
: "${BASE_PX:=85979.70}"           # 60/180 최적점이 낸 진입가  (85979.70711050 → 버림)
: "${VAR_PX:=85795.14}"            # 120/360 최적점이 낸 진입가 (85795.14865650 → 버림)
# 손절은 **두 팔을 같은 값으로 묶는다** — 규칙 문서의 보호 손절 5% 는 두 설정이 같은데 진입가가
# 달라 손절선이 갈린다. 한 칸만 남기려고 국면 호가 기준 하나로 고정한다.
: "${STOP_PX:=81605.00}"
WORK="${WORK_DIR:-./ab-$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' "$@"; }

# ── ① 심기 — 한 칸만 다른 두 팔을 **연달아** 던진다 ──────────────────────────
# 한 줄 = client_order_id | arm | limit_price | 격자 | PF | 거래 수 | 비용 가정
PLAN='
'"$RUN_ID"'-60-180|base|'"$BASE_PX"'|60/180|0.5123854981235674|68|assumed
'"$RUN_ID"'-120-360|variant|'"$VAR_PX"'|120/360|0.678789397136766|33|measured
'

echo 'client_order_id,tracking,arm,grid,profit_factor,total_trades,cost_basis' > "$WORK/tool_log.csv"
echo "① 심기 — tracking=$RUN_ID · 바꾸는 칸은 limit_price 하나"
while IFS='|' read -r COID ARM PX GRID PF TRADES COST; do
  [ -n "${COID:-}" ] || continue
  # ↓ 서버로 가는 본문. grid/profit_factor/total_trades 는 **여기 없다**.
  #   metadata 에는 **float 를 쓸 수 없다** (결정론) — 숫자는 문자열로 보낸다.
  #   float 를 넣으면 접수 전에 `422 invalid_metadata` 이고 `float_paths` 가 어디인지 알려 준다.
  BODY=$(jq -n --arg v "$VENUE" --arg s "$SYMBOL" --arg c "$COID" --arg t "$RUN_ID" \
               --arg px "$PX" --arg sl "$STOP_PX" --arg a "$ARM" \
               --argjson sec "$CLOSE_S" --argjson mh "$MAX_H" \
    '{venue:$v, symbol:$s, client_order_id:$c, side:"long",
      type:"limit", limit_price:$px,
      client_tracking_id:$t, tags:["ab-compare","a2-04-14"],
      exit_plan:{stop_loss:$sl, close_after_seconds:$sec},
      max_duration_hours:$mh,
      metadata:{arm:$a, changed_field:"limit_price"}}')
  RESP=$(api -X POST "$WELTIQ_BASE_URL/v1/trial-buys" -d "$BODY")
  echo "   $COID ($ARM, $PX) → $(echo "$RESP" | jq -r '.test_id // .code // "?"')"
  # ↓ 도구 쪽 로그. **서버에는 안 보낸다** — 이게 이 레시피의 전부다.
  printf '%s,%s,%s,%s,%s,%s,%s\n' "$COID" "$RUN_ID" "$ARM" "$GRID" "$PF" "$TRADES" "$COST" \
    >> "$WORK/tool_log.csv"
done <<< "$PLAN"

# 관측 창 + 여유. **미체결로 남은 팔은 여기서 아무것도 안 낸다** — 상대 시한의 기준이
# 「첫 진입 체결의 공개 시각」이라 안 채워지면 close_after_seconds 시계가 시작조차 안 한다.
echo "   (체결·청산이 돌 시간을 준다: $((CLOSE_S + 15))초)"; sleep "${SETTLE_S:-$((CLOSE_S + 15))}"

# ── ② 회수 — 추적 ID 로 두 팔을 한 번에 되찾는다 ─────────────────────────────
# `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 ' ')건"
while read -r TID; do
  api "$WELTIQ_BASE_URL/v1/forward-tests/$TID" > "$WORK/detail-$TID.json"
  # 미체결 팔은 status 가 running/entering 에서 멈춘다. 그게 이 편의 결과다.
  echo "   $TID → $(jq -r '.status' "$WORK/detail-$TID.json")"
done < "$WORK/test_ids.txt"

# ── ③ 전체 CSV — 시험마다 부르지 않는다 ──────────────────────────────────────
# 계정 전체 주문 엔드포인트가 추적 ID 필터를 받는다. 종전에는 「목록 1 + 시험마다 1」이었다.
# format=csv 는 필터에 걸린 전부를 커서로 끝까지 돌아 한 번에 낸다 (limit·before 를 쓰지 않는다).
# 열은 시험 하나 내보내기와 **같다** — 두 CSV 를 이어붙여도 안 깨진다.
echo '③ 전체 CSV — 추적 ID 로 걸러 한 번에'
api "$WELTIQ_BASE_URL/v1/forward-tests/orders?tracking=$RUN_ID&format=csv" > "$WORK/all.csv"
echo "   $(($(wc -l < "$WORK/all.csv") - 1))행"

# ── ④ 조인 — 추적 ID + client_order_id ───────────────────────────────────────
# **awk 를 쓰지 마라.** CSV 의 metadata·exit_plan·tags 칸에 **인용된 쉼표**가 들어 있어
# `-F,` 로 자르면 열이 밀린다 (조용히 밀리므로 더 나쁘다). 조인은 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 []) + ['arm', 'grid', 'profit_factor', 'total_trades', '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(arm=m['arm'] if m else '', grid=m['grid'] if m else '',
             profit_factor=m['profit_factor'] if m else '',
             total_trades=m['total_trades'] if m else '', join='matched' if m else 'unmatched')
    matched += bool(m)
    out.writerow(r)
    t = per.setdefault(r['test_id'], {'arm': '', 'grid': '',
                                      'state': '', 'enter': None, 'exit': None, 'reason': ''})
    if m:
        t['arm'], t['grid'] = m['arm'], m['grid']
    if r['action'] == 'enter':
        t['state'] = r['state']
    t['reason'] = r['close_reason'] or t['reason']
    if r['fill_qty']:
        t['enter' if r['action'] == 'enter' else 'exit'] = (float(r['fill_px']), float(r['fee'] or 0))
print(f'   조인됨 {matched} / 안 됨 {len(rows) - matched}')
if len(rows) - matched:
    print('   안 되는 행이 있다면 추적 ID 를 안 물려받은 자동 청산이다. 2026-09-28 부터 진입의\n'
          '   추적 ID·태그를 청산이 물려받으므로, 지금은 0 이어야 정상이다.')
print(f'\n   {"팔":<10}{"격자":<10}{"상태":<12}{"진입 체결":>16}{"청산 사유":>12}')
for t in per.values():
    if not t['arm']:
        continue
    px = f"{t['enter'][0]:.8f}" if t['enter'] else '미체결'
    print(f"   {t['arm']:<10}{t['grid']:<10}{t['state']:<12}{px:>16}{t['reason'] or '-':>12}")
print('\n   미체결로 남은 팔에는 close_reason 도, 손익도, 수수료도 없다.')
print('   그 팔이 **자기가 본 호가**도 기록에 안 남긴다 — 접수 응답에 bid/ask 가 없다.')
print('   같은 호가였다는 증거는 체결된 팔들의 fills[].published_at 이 밀리초까지 같다는 것뿐이고,')
print('   그건 정황이지 식별이 아니다 (호가 스냅샷 id 가 없다).')
PY

echo
echo "끝. HTTP 호출 $CALLS 회 · 한도 소모 2회 · 산출물 $WORK/{tool_log,all,joined}.csv"
echo "   대조군을 더 세우려면 PLAN 에 줄을 더한다 — **N팔 = N회**다."
