#!/usr/bin/env bash
# 09편 — freqtrade(04) 의 **손절 폭 한 칸만** 바꾼 3팔을 같은 시각에 내서 나란히 본다.
#
#   ┌─ 설계 원칙 ────────────────────────────────────────────────────────────┐
#   │ **의도는 도구가 갖는다.** weltiq 는 집행 기록만 갖고, 전략 이름·`stoploss` 비율·     │
#   │ 왜 그 폭을 골랐는지는 **저장하지 않는다.**                                      │
#   │ 서버로 가는 것은 client_order_id · client_tracking_id · metadata.arm 뿐이고,     │
#   │ 비율과 근거는 tool_log.csv 에만 쓴다. 둘을 잇는 것은 **추적 ID 하나**다.          │
#   └───────────────────────────────────────────────────────────────────────┘
#
# 이 레시피가 채우는 빈칸:
#   freqtrade 에서 손절 폭을 바꾸려면 **봇을 재시작해야 한다** — `reload_config` 는 이미 열린
#   포지션의 손절선을 안 바꾼다. 그래서 두 폭을 비교하면 회차가 갈리고 진입가도 갈린다
#   (원 기록: −0.2% 회차 진입 85037.82 · −0.05% 회차 진입 85198.18).
#   **같은 시각·같은 호가에 폭만 다른 N팔을 함께 내면** 진입이 한 점으로 고정되고,
#   차이가 그 칸에서만 난다.
#
# 쓰는 법:
#   export WELTIQ_API_KEY='wk_...'        # scope: run-forward-tests, read-data
#   export RUN_ID='ft-stop-ab-1'
#   ./09-trial-order-freqtrade-stoploss-ab.sh
#
# 주의: **대조군 N개 = 한도 N회.** 아래는 3팔 = 3회. 청산·재전송(200)·거절(409)은 0회다.
#       pro 월 1,000회 기준이면 3팔짜리 대조는 월 333회까지.
#       **실행하지 않았다.** 이 파일은 `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:=ft-stop-ab-$(date +%Y%m%d-%H%M%S)}"
: "${VENUE:=binance-spot}"
: "${SYMBOL:=BTCUSDT}"
: "${HOLD_S:=45}"                       # 세 팔 공통 시한. 한 칸만 다르게 하려면 나머지는 다 같아야 한다.
WORK="${WORK_DIR:-./cf-09-$RUN_ID}"

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

# ── ⓪ 틱 반올림 — 왜 원값이 422 인가 ────────────────────────────────────────
# freqtrade 가 내는 것은 **절대 가격이 아니라 비율**이다: `{"stoploss": -0.0005}`.
# 비율 × 기록된 진입가 85198.18 = `85155.590909` — binance-spot BTCUSDT 틱 0.01 의 배수가 아니다
#   → `422 invalid_stop_loss "85155.590909 is not a multiple of tick 0.01"`.
# 서버는 조용히 반올림해 주지 않는다. **비율로 손절하는 모든 도구가 이걸 만난다.**
# 부수 사실 하나: **시한 검증이 가격 검증보다 앞선다** — 시한을 빼면 비틱 레벨이 있어도
# `422 exit_plan_deadline_required` 가 먼저 온다. 레벨 오류를 보려면 시한을 먼저 채워야 한다.
BASE_PX=85198.18                         # 원 기록의 진입가 (RUN-LOG:366 `open_rate`)
tick_floor() { python3 -c "import sys;print(f'{__import__(\"decimal\").Decimal(sys.argv[1]).quantize(__import__(\"decimal\").Decimal(\"0.01\"), rounding=\"ROUND_DOWN\"):f}')" "$1"; }
# 롱 손절은 **버림**이 보수적이다 — 손절선이 내려가면 더 늦게 걸린다.
# (더 가깝게 걸고 싶으면 ROUND_UP 으로 바꿔라. 어느 쪽이든 **그 방향을 글에 적는다.**)

# ── ① 세 팔 — 손절 폭 한 칸만 다르다 ────────────────────────────────────────
# 한 줄 = arm | stoploss 비율 | 출처
#   기록  −0.0005  — `reports/a/04-freqtrade/tight-stop.json` · RUN-LOG:348-366
#                    (91초 만에 `Reason: stop_loss`, close_rate 85145.81)
#   합성  −0.0010  — 두 기록 사이를 채운 값. **하네스가 고른 값이고 도구 기록이 아니다.**
#   기록  −0.0020  — `docs/tutorials/a/04-freqtrade.md:197` (10분간 미발동)
ARMS='
tight|-0.0005|reports/a/04-freqtrade/tight-stop.json
mid|-0.0010|synthesized-by-harness
wide|-0.0020|docs/tutorials/a/04-freqtrade.md:197
'

echo 'client_order_id,tracking,arm,stoploss_pct,stop_level,source' > "$WORK/tool_log.csv"
echo "① 심기 — tracking=$RUN_ID · 세 팔을 **동시에** 던진다"
PIDS=()
while IFS='|' read -r ARM PCT SRC; do
  [ -n "${ARM:-}" ] || continue
  LEVEL=$(tick_floor "$(python3 -c "print($BASE_PX * (1 + $PCT))")")
  COID="$RUN_ID-$ARM"
  # ↓ 서버로 가는 본문.
  #   - `client_tracking_id` 는 **세 팔 모두 같다** — 이게 실험 하나를 잇는 실이다.
  #   - `metadata.arm` 이 팔을 가른다. metadata 에 **float 금지** — 전부 문자열이다
  #     (canonical_json 이 결정론 규칙으로 float 을 거부한다: 500).
  #   - `changed_field` 를 적어 둔다. 「한 칸만 바꿨다」를 나중에 증명하는 것은 이 줄이다.
  BODY=$(jq -n --arg v "$VENUE" --arg s "$SYMBOL" --arg c "$COID" --arg t "$RUN_ID" \
               --arg lv "$LEVEL" --arg a "$ARM" --arg p "$PCT" --arg src "$SRC" \
               --argjson sec "$HOLD_S" \
    '{venue:$v, symbol:$s, client_order_id:$c, side:"long", type:"market",
      client_tracking_id:$t, tags:["ab-compare","a2-09"],
      exit_plan:{stop_loss:$lv, close_after_seconds:$sec},
      max_duration_hours:1,
      metadata:{arm:$a, changed_field:"exit_plan.stop_loss",
                stoploss_pct:$p, source:$src, tool:"04-freqtrade"}}')
  # 백그라운드로 던져 세 건이 **같은 호가 국면 안에서** 접수되게 한다.
  # 접수는 사용자 단위로 직렬화되지만(pg_advisory_xact_lock) 실측에서 6건이 6.5ms 안에 통과했다 —
  # 실질 병목이 아니다.
  ( api -X POST "$WELTIQ_BASE_URL/v1/trial-buys" -d "$BODY" > "$WORK/resp-$ARM.json" ) &
  PIDS+=("$!")
  printf '%s,%s,%s,%s,%s,%s\n' "$COID" "$RUN_ID" "$ARM" "$PCT" "$LEVEL" "$SRC" >> "$WORK/tool_log.csv"
done <<< "$ARMS"
for P in "${PIDS[@]}"; do wait "$P"; done
for F in "$WORK"/resp-*.json; do
  echo "   $(basename "$F" .json) → $(jq -r '.test_id // .error // "?"' "$F")"
done

echo "   (체결·청산이 돌 시간을 준다: $((HOLD_S + 60))초)"; sleep "$((HOLD_S + 60))"

# ── ② 회수 — tracking= 으로 계정 전체에서 이 실험만 ─────────────────────────
echo "② 회수 — tracking=$RUN_ID"
api "$WELTIQ_BASE_URL/v1/forward-tests?tracking=$RUN_ID&limit=200" > "$WORK/list.json"
echo "   시험 $(jq -r '.tests | length' "$WORK/list.json")건 (세 팔이면 3이어야 한다)"

# ── ③ 집행 기록 — 계정 전체 CSV 한 번에 ─────────────────────────────────────
# 자동 청산 행이 진입의 추적 ID·태그를 물려받으므로 한 번에 {enter, exit} 가 같이 온다.
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 []) + ['arm', 'stoploss_pct', '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 '', stoploss_pct=m['stoploss_pct'] if m else '',
             join='matched' if m else 'unmatched')
    matched += bool(m)
    out.writerow(r)
    t = per.setdefault(r['test_id'], {'arm': '', 'pct': '', 'level': '', 'enter': None,
                                      'exit': None, 'reason': '', 'trig': '', 'trig_px': '',
                                      'pub': ''})
    if m:
        t.update(arm=m['arm'], pct=m['stoploss_pct'], level=m['stop_level'])
    for k, c in (('reason', 'close_reason'), ('trig', 'exit_trigger'), ('trig_px', 'exit_trigger_px')):
        if r.get(c):
            t[k] = r[c]
    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))
        if r['action'] == 'enter':
            t['pub'] = r.get('published_at', '')
print(f'   조인됨 {matched} / 안 됨 {len(rows) - matched}')
# unmatched = 서버가 만든 자동 청산 행. client_order_id 가 "exit-plan:…" 이라 도구 로그에 있을 수 없다.

arms = [t for t in per.values() if t['arm']]
print(f'\n   {"팔":<7}{"손절%":>8}{"손절선":>12}{"진입체결가":>18}{"진입 published_at":>28}'
      f'{"exit_trigger":>14}{"trigger_px":>12}{"close_reason":>14}{"순손익":>16}')
for t in sorted(arms, key=lambda x: x['arm']):
    if not (t['enter'] and t['exit']):
        print(f'   {t["arm"]:<7}{t["pct"]:>8}{t["level"]:>12}{"미체결":>18}{t["pub"] or "—":>28}'
              f'{"—":>14}{"—":>12}{t["reason"] or "—":>14}{"미판정":>16}')
        continue
    fee = t['enter'][2] + t['exit'][2]
    net = t['exit'][0] * t['exit'][1] - t['enter'][0] * t['enter'][1] - fee
    print(f'   {t["arm"]:<7}{t["pct"]:>8}{t["level"]:>12}{t["enter"][1]:>18.7f}{t["pub"]:>28}'
          f'{t["trig"] or "—":>14}{t["trig_px"] or "—":>12}{t["reason"]:>14}{net:>16.12f}')

# ── 「같은 호가에서 나갔다」를 무엇으로 증명하나 ──────────────────────────────
# 호가 스냅샷 식별자는 **없다.** 현재의 최선은 진입 체결의 `published_at` 이 팔마다
# **밀리초까지 같은 것**이고, 그건 정황이지 식별이 아니다.
pubs = {t['pub'] for t in arms if t['pub']}
print(f'\n   진입 체결이 참조한 호가의 published_at 종류: {len(pubs)} (1이면 한 호가가 전부를 채웠다)')
for p in sorted(pubs):
    print(f'     {p}')
if any(not t['enter'] for t in arms):
    print('   ⚠ 미체결 팔이 있다. **미체결 팔은 자기가 본 호가가 기록에 안 남는다** —')
    print('     reference_px 는 접수 시점의 기준가일 뿐이고 published_at 은 체결 뒤에만 생긴다.')
print('   (진입가·수량·시한이 팔마다 같고 손절선만 다른지 joined.csv 에서 직접 확인하라.')
print('    한 칸만 바꿨다는 주장은 metadata.changed_field 가 아니라 이 대조가 뒷받침한다.)')
PY

echo
echo "끝. 산출물 $WORK/{tool_log,all,joined}.csv · 한도 소모 3회 (N팔 = N회)"
