"""
WorldQuant Brain Alpha Machine — User 版本核心函式庫

提供 Alpha 挖掘自動化流程：登入認證、資料集/欄位獲取、Alpha 表達式生成（一階/二階/三階）、
批量回測提交、篩選剪枝、提交資格檢查。

版本差異：
- User：single_simulate() 每次提交 1 個 Alpha（本檔案）
- Consultant：multi_simulate() 每次提交 10 個 Alpha（保留供參考）

使用方式：`from machine_lib import *` → `s = login()`
注意：使用前請先在 login() 中填入帳號密碼，建議搭配 Alpha Machine.ipynb 使用。
"""

import requests
from os import environ
from time import sleep
import time
from datetime import datetime
import json
import pandas as pd
import random
import pickle
from itertools import product
from itertools import combinations
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urljoin
import pickle


class SessionExpiredError(RuntimeError):
    """JWT 過期或 401 — 呼叫方須手動重登。"""


def _raise_if_unauthorized(resp, where: str):
    if resp.status_code in (401, 403):
        raise SessionExpiredError(
            f"{where}: HTTP {resp.status_code} — JWT expired or unauthorized. "
            f"Re-login manually then retry. body[:200]={resp.text[:200]}"
        )


HTTP_TIMEOUT = (10, 60)
WQ_BASE_URL = 'https://api.worldquantbrain.com'


# ============================================================================
# WQSession — requests.Session 子類，封裝 base_url + auth + 自動 401 偵測
# （Wave 3.1，靈感自 RussellDash332/WQ-Brain，但保留 free-function API 不破壞既有 caller）
# ============================================================================

class WQSession(requests.Session):
    """requests.Session 子類；提供：
    - `s.url(path)` 把 '/users/self' 拼到 BASE_URL（短路徑寫法）
    - 自動偵測 401/403 → SessionExpiredError（caller 不必每次手動 _raise_if_unauthorized）
    - `s.username` 屬性（若 login 過會記下，給 GUI 顯示用）

    既有 free-function（login()/get_datasets()/get_alphas()...）回傳的物件仍是 plain
    requests.Session，與本子類並存。caller 想升級就用 WQSession()，不想用就保持原樣。
    """

    def __init__(self, base_url: str = WQ_BASE_URL,
                 timeout=HTTP_TIMEOUT, auto_raise_unauth: bool = True):
        super().__init__()
        self._base_url = base_url.rstrip('/')
        self._default_timeout = timeout
        self._auto_raise_unauth = auto_raise_unauth
        self.username: str = ''

    def url(self, path: str) -> str:
        if path.startswith('http://') or path.startswith('https://'):
            return path
        if not path.startswith('/'):
            path = '/' + path
        return self._base_url + path

    def request(self, method, url, *args, **kwargs):
        # 短路徑自動拼 base url
        url = self.url(url)
        kwargs.setdefault('timeout', self._default_timeout)
        resp = super().request(method, url, *args, **kwargs)
        if self._auto_raise_unauth and resp.status_code in (401, 403):
            raise SessionExpiredError(
                f"{method} {url}: HTTP {resp.status_code} — JWT expired or unauthorized. "
                f"body[:200]={resp.text[:200]}"
            )
        return resp

    def login_basic(self, username: str, password: str):
        """Basic→JWT 登入。失敗拋 SessionExpiredError；成功後 self.auth 仍保留。"""
        self.auth = (username, password)
        # 直接 POST，避開 auto_raise（因為 401 在這裡是「帳密錯」不是 JWT 過期）
        old = self._auto_raise_unauth
        self._auto_raise_unauth = False
        try:
            r = self.post('/authentication')
        finally:
            self._auto_raise_unauth = old
        if r.status_code in (401, 403):
            raise SessionExpiredError(
                f"WQ login 失敗（HTTP {r.status_code}）：帳密錯誤或需要 biometric。"
                f" body[:200]={r.text[:200]}"
            )
        self.username = username
        return r

# ============================================================================
# 全域變數：操作符定義
# ============================================================================

# 基礎操作符：標準化 / 排名 / 分位數 / 歸一化
basic_ops = ["reverse", "inverse", "rank", "zscore", "quantile", "normalize"]

# 時間序列操作符：對時間窗口內的資料做統計
ts_ops = ["ts_rank", "ts_zscore", "ts_delta",  "ts_sum", "ts_delay",
          "ts_std_dev", "ts_mean",  "ts_arg_min", "ts_arg_max", "ts_scale", "ts_quantile"]

# 合併供 first_order_factory 使用
ops_set = basic_ops + ts_ops

# ============================================================================
# 1. 認證與登入
# ============================================================================

def login():
    """登入 WorldQuant Brain，建立認證 Session。

    帳密來源（依序嘗試）：
    1. 環境變數 WQ_USERNAME / WQ_PASSWORD
    2. ~/.wq_gui_credentials.json（GUI 寫入，chmod 600）格式：{"username": "...", "password": "..."}
    3. 都沒有 → 拋出 RuntimeError 並印出設定指引

    回傳 requests.Session。失敗（INVALID_CREDENTIAL）時拋 SessionExpiredError。
    """
    import json
    from pathlib import Path

    username = environ.get("WQ_USERNAME", "").strip()
    password = environ.get("WQ_PASSWORD", "").strip()

    if not (username and password):
        creds_path = Path.home() / ".wq_gui_credentials.json"
        if creds_path.exists():
            try:
                data = json.loads(creds_path.read_text(encoding="utf-8"))
                username = (data.get("username") or "").strip()
                password = (data.get("password") or "").strip()
            except (json.JSONDecodeError, OSError) as e:
                raise RuntimeError(
                    f"讀取 {creds_path} 失敗：{e}。請檢查 JSON 格式。"
                ) from e

    if not (username and password):
        raise RuntimeError(
            "找不到 WQ Brain 帳密。請選一種設定方式：\n"
            "  方式 1（推薦）：執行一次 GUI 並勾「記住帳密」，會自動寫入 "
            "~/.wq_gui_credentials.json（chmod 600）\n"
            "  方式 2：設環境變數 WQ_USERNAME / WQ_PASSWORD\n"
            "  方式 3：手動建立 ~/.wq_gui_credentials.json，內容："
            '{"username": "your@email", "password": "your_password"}，'
            "並執行 chmod 600 ~/.wq_gui_credentials.json"
        )

    s = requests.Session()
    # Perf #4：HTTPAdapter pool + keepalive，省每 request 200-400ms TLS 重握
    _adapter = requests.adapters.HTTPAdapter(
        pool_connections=10, pool_maxsize=20, max_retries=0)
    s.mount('https://', _adapter)
    s.mount('http://', _adapter)
    s.headers.update({'Connection': 'keep-alive'})
    s.auth = (username, password)
    response = s.post('https://api.worldquantbrain.com/authentication', timeout=HTTP_TIMEOUT)
    if response.status_code in (401, 403):
        raise SessionExpiredError(
            f"WQ login 失敗（HTTP {response.status_code}）：帳密錯誤或帳號被鎖。"
            f" body[:200]={response.text[:200]}"
        )
    return s

# ============================================================================
# 2. 資料集與資料欄位獲取
# ============================================================================

def get_datasets(
    s,
    instrument_type: str = 'EQUITY',
    region: str = 'USA',
    delay: int = 1,
    universe: str = 'TOP3000'
):
    """獲取使用者有權限的資料集列表，回傳 pandas.DataFrame（含 id/name/description 等）。

    region 可選：USA / CHN / TWN / HKG / KOR / JPN / EUR
    delay：1=T+1（預設）；0=即時（需特殊權限）
    universe：TOP3000 / TOP2000 / TOP1000
    """
    url = "https://api.worldquantbrain.com/data-sets?" + \
        f"instrumentType={instrument_type}&region={region}&delay={str(delay)}&universe={universe}"
    result = s.get(url)
    datasets_df = pd.DataFrame(result.json()['results'])
    return datasets_df


def get_datafields(
    s,
    instrument_type: str = 'EQUITY',
    region: str = 'USA',
    delay: int = 1,
    universe: str = 'TOP3000',
    dataset_id: str = '',
    search: str = ''
):
    """獲取指定資料集的所有欄位（自動分頁，每頁 50 筆）。

    若指定 search：使用搜尋 API（固定 100 筆上限）；
    否則：先取得 count，再分頁載入全部 dataset_id 下欄位。
    回傳 DataFrame 含 id/name/type（MATRIX / VECTOR）/description/dataset 等。
    """
    if len(search) == 0:
        url_template = "https://api.worldquantbrain.com/data-fields?" + \
            f"&instrumentType={instrument_type}" + \
            f"&region={region}&delay={str(delay)}&universe={universe}&dataset.id={dataset_id}&limit=50" + \
            "&offset={x}"
        probe = s.get(url_template.format(x=0), timeout=30)
        if probe.status_code != 200:
            raise RuntimeError(
                f"data-fields probe failed: HTTP {probe.status_code} "
                f"dataset_id={dataset_id} region={region} delay={delay} universe={universe}\n"
                f"body[:500]={probe.text[:500]}"
            )
        try:
            probe_json = probe.json()
        except ValueError:
            raise RuntimeError(f"data-fields probe returned non-JSON: {probe.text[:500]}")
        if 'count' not in probe_json:
            raise RuntimeError(
                f"data-fields probe missing 'count'. "
                f"Likely causes: (1) JWT expired → re-login manually, "
                f"(2) dataset_id={dataset_id!r} not granted at delay={delay} "
                f"universe={universe} region={region}, (3) rate-limited.\n"
                f"response keys={list(probe_json.keys())} body[:500]={probe.text[:500]}"
            )
        count = probe_json['count']
    else:
        url_template = "https://api.worldquantbrain.com/data-fields?" + \
            f"&instrumentType={instrument_type}" + \
            f"&region={region}&delay={str(delay)}&universe={universe}&limit=50" + \
            f"&search={search}" + \
            "&offset={x}"
        count = 100  # 搜尋模式固定回傳 100 筆

    datafields_list_flat = []
    for x in range(0, count, 50):
        page = s.get(url_template.format(x=x), timeout=30)
        if page.status_code != 200:
            raise RuntimeError(
                f"data-fields page offset={x} failed HTTP {page.status_code}: {page.text[:300]}"
            )
        results = page.json().get('results', [])
        if not results:
            break  # 防止 count 與實際資料不一致導致空頁迴圈
        datafields_list_flat.extend(results)

    return pd.DataFrame(datafields_list_flat)

# ============================================================================
# 3. 資料欄位前處理
# ============================================================================

def get_vec_fields(fields):
    """將 VECTOR 類型欄位透過 vec_avg / vec_sum 等聚合操作符轉為標量表達式。

    vec_choose 特殊處理：分別生成 nth=-1 / nth=0 兩版本。
    若有其他 VECTOR 操作符權限，請自行加入 vec_ops。
    """
    # ⚠️ 請在此處添加您獲得權限的 Vector 操作符
    vec_ops = ["vec_avg", "vec_sum"]
    vec_fields = []

    for field in fields:
        for vec_op in vec_ops:
            if vec_op == "vec_choose":
                vec_fields.append("%s(%s, nth=-1)" % (vec_op, field))
                vec_fields.append("%s(%s, nth=0)" % (vec_op, field))
            else:
                vec_fields.append("%s(%s)" % (vec_op, field))

    return(vec_fields)


def process_datafields(df):
    """前處理 API 取得的原始欄位 DataFrame，輸出可直接餵給 factory 的表達式列表。

    流程：
    1. 提取 MATRIX 欄位 id
    2. VECTOR 欄位透過 get_vec_fields() 轉換
    3. 對每個欄位套用 ts_backfill(field, 120) 回填 + winsorize(std=4) 去極值
    """
    datafields = []
    datafields += df[df['type'] == "MATRIX"]["id"].tolist()
    datafields += get_vec_fields(df[df['type'] == "VECTOR"]["id"].tolist())

    return ["winsorize(ts_backfill(%s, 120), std=4)" % field for field in datafields]

# ============================================================================
# 4. Alpha 表達式生成（Factories）
# ============================================================================

def ts_factory(op, field):
    """為時間序列操作符生成多窗口表達式（5/22/66/120/240 天）。"""
    import re as _re
    _WIN_RE = _re.compile(r",\s*(\d+)\s*[,)]")
    field_long_windows = {int(m) for m in _WIN_RE.findall(field) if int(m) >= 100}
    output = []
    days = [5, 22, 66, 120, 240]

    for day in days:
        # Skip if this would create a second distinct long window (timeout_risk)
        if field_long_windows and day >= 100 and day not in field_long_windows:
            continue
        alpha = "%s(%s, %d)" % (op, field, day)
        output.append(alpha)

    return output


def shape_factory(fields, region='USA', cross_field_pairs=None, ratio_field_pairs=None,
                  news_vec_fields=None):
    """CrisperX-50 + hr-23 啟發的 6 個 wrapper shape（Wave 2.1）。

    每個 field 會產出 6 種 shape 變體；額外的 cross/ratio/news 需要 caller 提供配對。

    Shape A — `ts_mean(field, 10)` 10 日平滑（CrisperX 中 32% alpha 用此 wrapper，pool 嚴重缺）
    Shape B — `quantile(ts_rank(field, 250))` 兩段非線性（hr-23 風格）
    Shape D — same-field 多 knob 變體（不同窗口 + 不同 ts wrapper 切換）
    Shape A/D 對所有 field 都適用，自動發。

    Shape C — `ts_av_diff(F1,N) * ts_corr(F1,F2,N)` cross-field（fitness 1.77）
              caller 傳 cross_field_pairs=[(F1,F2,N), ...] 才會發

    Shape E — `A / max(B, 1e-6)` ratio-of-two（hr-23 ebit/capex 風格）
              caller 傳 ratio_field_pairs=[(A,B), ...] 才會發

    Shape F — `ts_backfill(vec_avg(nws), 504)` VECTOR escape
              caller 傳 news_vec_fields=[...] 才會發

    每個表達式回 raw form（無 winsorize / ts_backfill 包覆，因為 shape 自帶長窗口或聚合）。
    """
    out = []

    # Shape A：10 日平滑（最高 ROI）
    for field in fields:
        out.append("ts_mean(%s, 10)" % field)

    # Shape B：quantile(ts_rank(field, 250)) 兩段非線性
    for field in fields:
        out.append("quantile(ts_rank(%s, 250))" % field)

    # Shape D：same-field 多 knob 變體
    # 變體 1：rank(ts_zscore(field, 60))
    # 變體 2：-ts_rank(field, 250)
    # 變體 3：zscore(ts_delta(field, 90))
    for field in fields:
        out.append("rank(ts_zscore(%s, 60))" % field)
        out.append("-ts_rank(%s, 250)" % field)
        out.append("zscore(ts_delta(%s, 90))" % field)

    # Shape C：cross-field（caller 提供 (F1, F2, N)）
    if cross_field_pairs:
        for f1, f2, n in cross_field_pairs:
            out.append("ts_av_diff(%s, %d) * ts_corr(%s, %s, %d)" % (f1, n, f1, f2, n))

    # Shape E：ratio-of-two
    if ratio_field_pairs:
        for a, b in ratio_field_pairs:
            # 1e-6 用十進位避免 sci-notation invalid bug
            out.append("%s / max(%s, 0.000001)" % (a, b))

    # Shape F：news VECTOR escape
    if news_vec_fields:
        for field in news_vec_fields:
            out.append("ts_backfill(vec_avg(%s), 504)" % field)

    return out


def first_order_factory(fields, ops_set):
    """一階 Alpha 工廠：欄位 × 操作符組合產出基礎 Alpha 表達式。

    分派規則：
    - ts_percentage / ts_decay_exp_window / ts_moment / ts_entropy → ts_comp_factory（需額外參數）
    - 其他 ts_* 與 inst_tvr → ts_factory（標準時間窗口）
    - vector* → vector_factory
    - signed_power → 固定指數 2
    - 其他 basic_ops → 直接包裝
    每個欄位也會以原樣加入一份。
    """
    alpha_set = []

    for field in fields:
        alpha_set.append(field)
        # alpha_set.append("-%s"%field)  # 可選反向

        for op in ops_set:
            if op == "ts_percentage":
                alpha_set += ts_comp_factory(op, field, "percentage", [0.5])
            elif op == "ts_decay_exp_window":
                alpha_set += ts_comp_factory(op, field, "factor", [0.5])
            elif op == "ts_moment":
                alpha_set += ts_comp_factory(op, field, "k", [2, 3, 4])
            elif op == "ts_entropy":
                alpha_set += ts_comp_factory(op, field, "buckets", [10])
            elif op.startswith("ts_") or op == "inst_tvr":
                alpha_set += ts_factory(op, field)
            elif op.startswith("vector"):
                alpha_set += vector_factory(op, field)
            elif op == "signed_power":
                alpha = "%s(%s, 2)" % (op, field)
                alpha_set.append(alpha)
            else:
                alpha = "%s(%s)" % (op, field)
                alpha_set.append(alpha)

    return alpha_set

# ============================================================================
# 5. 任務池與回測（User 版本）
# ============================================================================

def load_task_pool_single(alpha_list, limit_of_single_simulations):
    """將 (alpha, decay) 配對列表切成多個 task chunk，供 single_simulate() 使用。

    每個 chunk 大小為 limit_of_single_simulations（通常為 3）；最後一個可能小於 limit。
    """
    pool = [alpha_list[i:i + limit_of_single_simulations]
            for i in range(0, len(alpha_list), limit_of_single_simulations)]
    return pool


def _poll_progress(s, progress_url: str, max_wait: float = 1800.0):
    """阻塞輪詢單一 simulation progress URL 直到完成或 max_wait 超時。

    回傳 (progress_url, status, error)：status 為 'COMPLETE'/'WARNING'/...
    任何 401/403 直接 raise SessionExpiredError 中斷整個 batch（避免 silent death）。
    """
    deadline = time.monotonic() + max_wait
    while True:
        if time.monotonic() > deadline:
            return (progress_url, "TIMEOUT_LOCAL", f"exceeded {max_wait}s")
        try:
            r = s.get(progress_url, timeout=HTTP_TIMEOUT)
        except requests.exceptions.RequestException as e:
            return (progress_url, "NETWORK_ERROR", str(e))

        _raise_if_unauthorized(r, f"poll {progress_url}")

        retry_after = r.headers.get("Retry-After")
        if retry_after:
            try:
                wait = float(retry_after)
            except ValueError:
                wait = 5.0
            if wait <= 0:
                wait = 1.0  # 防 busy-loop
            sleep(min(wait, 30.0))
            continue

        try:
            status = r.json().get("status", "UNKNOWN")
        except ValueError:
            return (progress_url, "BAD_JSON", r.text[:200])
        return (progress_url, status, None)


def single_simulate(s, alpha_pool, neut, region, universe, start, max_workers: int = 4,
                    post_interval: float = 3.0):
    """執行單一模擬回測（User 版本核心函數）。

    Args:
        s: 已登入的 requests.Session（呼叫方負責 login，遵守 JWT 硬中斷規則）
        alpha_pool: load_task_pool_single() 回傳的任務池
        neut: 中性化層級
        region / universe: 市場參數
        start: 從第幾個 pool 開始（中斷恢復用）
        max_workers: 並發 polling worker 數（預設 4，POST 仍串行）
        post_interval: 兩次 POST 間最小間隔（秒，預設 3）— 避免 WQ 限流

    異常：
        SessionExpiredError → 401/403 立即拋出，呼叫方需手動重登
        其他網路錯誤 → 記錄但不中斷整個 batch
    """
    for x, task in enumerate(alpha_pool):
        if x < start:
            continue

        progress_urls = []

        # 步驟 1：串行提交（嚴守 3 秒間隔，禁止並發 POST）
        for y, (alpha, decay) in enumerate(task):
            if y > 0:
                sleep(post_interval)
            simulation_data = {
                'type': 'REGULAR',
                'settings': {
                    'instrumentType': 'EQUITY',
                    'region': region,
                    'universe': universe,
                    'delay': 1,
                    'decay': decay,
                    'neutralization': neut,
                    'truncation': 0.08,
                    'pasteurization': 'ON',
                    'testPeriod': 'P0Y',
                    'unitHandling': 'VERIFY',
                    'nanHandling': 'ON',
                    'language': 'FASTEXPR',
                    'visualization': False,
                },
                'regular': alpha}

            try:
                resp = s.post('https://api.worldquantbrain.com/simulations',
                              json=simulation_data, timeout=HTTP_TIMEOUT)
            except requests.exceptions.RequestException as e:
                print(f"[task {x}] POST network error: {e}; skipping alpha")
                continue

            _raise_if_unauthorized(resp, f"POST /simulations task={x} alpha={y}")

            if resp.status_code == 429:
                wait = float(resp.headers.get("Retry-After", "60"))
                print(f"[task {x}] rate-limited, sleep {wait:.0f}s")
                sleep(wait)
                continue

            loc = resp.headers.get('Location')
            if not loc:
                print(f"[task {x}] no Location header, status={resp.status_code} body={resp.text[:200]}")
                continue
            progress_urls.append(loc)

        print(f"task {x} post done ({len(progress_urls)} submitted)")

        # 步驟 2：並發 polling 所有 progress URL
        if not progress_urls:
            continue
        with ThreadPoolExecutor(max_workers=min(max_workers, len(progress_urls))) as ex:
            futures = {ex.submit(_poll_progress, s, url): url for url in progress_urls}
            for fut in as_completed(futures):
                url = futures[fut]
                try:
                    _, status, err = fut.result()
                except SessionExpiredError:
                    raise  # JWT 過期 → 中斷整個 batch
                except Exception as e:
                    print(f"poll {url} crashed: {e}")
                    continue
                if status not in ("COMPLETE", "WARNING"):
                    print(f"Not complete [{status}]: {url} err={err}")

        print(f"task {x} simulate done")

    print("Simulate done")

# ============================================================================
# 6. Alpha 屬性管理
# ============================================================================

def set_alpha_properties(
    s,
    alpha_id,
    name: str = None,
    color: str = None,
    selection_desc: str = "None",
    combo_desc: str = "None",
    tags: str = ["ace_tag"],
):
    """修改 Alpha 的描述參數（名稱、顏色、標籤、Selection / Combo description）。

    用於組織管理大量 Alpha：例如為 Alpha 命名 'analyst4_momentum_22d'、加上
    ['momentum','analyst4'] 標籤、設定顏色供網頁分類。回傳 requests.Response。
    """
    params = {
        "color": color,
        "name": name,
        "tags": tags,
        "category": None,
        "regular": {"description": None},
        "combo": {"description": combo_desc},
        "selection": {"description": selection_desc},
    }
    response = s.patch(
        "https://api.worldquantbrain.com/alphas/" + alpha_id, json=params
    )

# ============================================================================
# 7. Alpha 篩選與獲取
# ============================================================================

def _normalize_date(d: str) -> str:
    # 'YYYY-MM-DD' → 原樣；'MM-DD' → 補當前年；其他 → 報錯
    if not isinstance(d, str):
        raise ValueError(f'date must be str, got {type(d).__name__}')
    if d.count('-') == 2 and len(d) >= 10:
        return d
    if d.count('-') == 1:
        return f'{datetime.now().year}-{d}'
    raise ValueError(f'unsupported date format: {d!r} (need YYYY-MM-DD or MM-DD)')


def get_alphas(s, start_date, end_date, sharpe_th, fitness_th, region, alpha_num, usage):
    """從 WQ Brain 拉取符合條件的 Alpha 列表，並依 Turnover 建議新的 decay 值。

    Args:
        start_date / end_date: 'YYYY-MM-DD' 或 'MM-DD' 格式
            （MM-DD 短格式會自動補上當前年份；舊版固定 2025 的 bug 已修復 2026-05）
        sharpe_th / fitness_th: 閾值（usage='track' 取 |Sharpe|>th；'submit' 僅取正向 Sharpe>th）
        usage: 'track'（追蹤潛力，含正負向）/ 'submit'（提交準備，僅正向）

    篩選條件：status ∈ {UNSUBMITTED, IS_FAIL}、Sharpe/Fitness 過閾、長短倉合計 > 100、type ≠ SUPER。
    若 Sharpe < 0 自動在表達式前加負號。

    Decay 建議邏輯（依 Turnover）：
        >0.7 → ×4 ；>0.6 → ×3+3 ；>0.5 → ×3 ；>0.4 → ×2 ；>0.35 → +4 ；>0.3 → +2

    回傳 list of [alpha_id, exp, sharpe, turnover, fitness, margin, dateCreated, decay, suggested_decay]。

    注意：呼叫方需傳入已登入的 session，不在 lib 內 re-login（JWT 硬中斷規則）。
    """
    output = []
    count = 0
    sd = _normalize_date(start_date)
    ed = _normalize_date(end_date)

    for i in range(0, alpha_num, 100):
        print(i)

        # 正向 Alpha：Sharpe > 閾值
        url_e = "https://api.worldquantbrain.com/users/self/alphas?limit=100&offset=%d" % (i) \
                + "&status=UNSUBMITTED%1FIS_FAIL&dateCreated%3E=" + sd  \
                + "T00:00:00-04:00&dateCreated%3C" + ed \
                + "T00:00:00-04:00&is.fitness%3E" + str(fitness_th) + "&is.sharpe%3E" \
                + str(sharpe_th) + "&settings.region=" + region + "&order=-is.sharpe&hidden=false&type!=SUPER"

        # 負向 Alpha：Sharpe < -閾值
        url_c = "https://api.worldquantbrain.com/users/self/alphas?limit=100&offset=%d" % (i) \
                + "&status=UNSUBMITTED%1FIS_FAIL&dateCreated%3E=" + sd  \
                + "T00:00:00-04:00&dateCreated%3C" + ed \
                + "T00:00:00-04:00&is.fitness%3C-" + str(fitness_th) + "&is.sharpe%3C-" \
                + str(sharpe_th) + "&settings.region=" + region + "&order=is.sharpe&hidden=false&type!=SUPER"

        urls = [url_e]
        if usage != "submit":
            urls.append(url_c)

        for url in urls:
            response = s.get(url, timeout=HTTP_TIMEOUT)
            _raise_if_unauthorized(response, f"GET alphas offset={i}")
            try:
                alpha_list = response.json()["results"]

                for j in range(len(alpha_list)):
                    alpha_id = alpha_list[j]["id"]
                    name = alpha_list[j]["name"]
                    dateCreated = alpha_list[j]["dateCreated"]
                    sharpe = alpha_list[j]["is"]["sharpe"]
                    fitness = alpha_list[j]["is"]["fitness"]
                    turnover = alpha_list[j]["is"]["turnover"]
                    margin = alpha_list[j]["is"]["margin"]
                    longCount = alpha_list[j]["is"]["longCount"]
                    shortCount = alpha_list[j]["is"]["shortCount"]
                    decay = alpha_list[j]["settings"]["decay"]
                    exp = alpha_list[j]['regular']['code']
                    count += 1

                    if (longCount + shortCount) > 100:
                        if sharpe < -sharpe_th:
                            exp = "-%s" % exp

                        rec = [alpha_id, exp, sharpe, turnover, fitness, margin, dateCreated, decay]
                        print(rec)

                        if turnover > 0.7:
                            rec.append(decay * 4)
                        elif turnover > 0.6:
                            rec.append(decay * 3 + 3)
                        elif turnover > 0.5:
                            rec.append(decay * 3)
                        elif turnover > 0.4:
                            rec.append(decay * 2)
                        elif turnover > 0.35:
                            rec.append(decay + 4)
                        elif turnover > 0.3:
                            rec.append(decay + 2)

                        output.append(rec)
            except (KeyError, TypeError, ValueError) as e:
                print(f"[get_alphas offset={i}] parse error: {e}; body[:200]={response.text[:200]}")

    print("count: %d" % count)
    return output

# ============================================================================
# 8. Alpha 剪枝
# ============================================================================

def prune(next_alpha_recs, prefix, keep_num):
    """以欄位前綴分組剪枝：每個欄位只保留 Sharpe 最高的前 keep_num 個 Alpha。

    從表達式中以 prefix split 後取首個逗號前段作為欄位識別；Sharpe 為負時欄位名加負號區分。
    輸入 [alpha_id, exp, sharpe, turnover, fitness, ..., decay, suggested_decay]，
    輸出 [exp, suggested_decay]。
    """
    output = []
    num_dict = defaultdict(int)

    for rec in next_alpha_recs:
        exp = rec[1]
        field = exp.split(prefix)[-1].split(",")[0]
        sharpe = rec[2]

        if sharpe < 0:
            field = "-%s" % field

        if num_dict[field] < keep_num:
            num_dict[field] += 1
            decay = rec[-1]
            exp = rec[1]
            output.append([exp, decay])

    return output

# ============================================================================
# 9. 二階 Alpha 生成（Group Operations）
# ============================================================================

def get_group_second_order_factory(first_order, group_ops, region):
    """二階 Alpha 工廠入口：對每個一階 Alpha × 每個 group_op 呼叫 group_factory()。

    group_ops 通常為 ['group_neutralize', 'group_rank', 'group_zscore']。
    """
    second_order = []
    for fo in first_order:
        for group_op in group_ops:
            second_order += group_factory(group_op, fo, region)
    return second_order


def group_factory(op, field, region):
    """對 Alpha 套用群組操作符，跨多種群組定義生成表達式。

    群組類型：
    - 基礎：market / sector / industry / subindustry
    - 自訂分桶：cap / asset / sector_cap / sector_asset / vol / liquidity（皆 0.1~1 step 0.1）
    - 區域專用：USA → 5 個 pv13_* sector 群組

    分派：group_vector → 加 vector 參數；group_percentage → percentage=0.5；
    其他 → 包 densify(group)。
    """
    output = []
    vectors = ["cap"]

    # USA 區域專用 pv13 群組
    usa_group_13 = ['pv13_h_min2_3000_sector', 'pv13_r2_min20_3000_sector',
                    'pv13_r2_min2_3000_sector', 'pv13_r2_min2_3000_sector',
                    'pv13_h_min2_focused_pureplay_3000_sector']

    cap_group = "bucket(rank(cap), range='0.1, 1, 0.1')"
    asset_group = "bucket(rank(assets),range='0.1, 1, 0.1')"
    sector_cap_group = "bucket(group_rank(cap, sector),range='0.1, 1, 0.1')"
    sector_asset_group = "bucket(group_rank(assets, sector),range='0.1, 1, 0.1')"

    vol_group = "bucket(rank(ts_std_dev(returns,20)),range = '0.1, 1, 0.1')"
    liquidity_group = "bucket(rank(close*volume),range = '0.1, 1, 0.1')"

    groups = ["market", "sector", "industry", "subindustry",
              cap_group, asset_group, sector_cap_group, sector_asset_group,
              vol_group, liquidity_group]

    groups += usa_group_13

    def _depth(expr: str) -> int:
        d = mx = 0
        for ch in expr:
            if ch == "(":
                d += 1
                mx = max(mx, d)
            elif ch == ")":
                d -= 1
        return mx

    for group in groups:
        if op.startswith("group_vector"):
            for vector in vectors:
                alpha = "%s(%s,%s,%s)" % (op, field, vector, group)
                if _depth(alpha) <= 5:
                    output.append(alpha)
        elif op.startswith("group_percentage"):
            alpha = "%s(%s,%s,percentage=0.5)" % (op, field, group)
            if _depth(alpha) <= 5:
                output.append(alpha)
        else:
            alpha = "%s(%s,%s)" % (op, field, group)
            if _depth(alpha) <= 5:
                output.append(alpha)

    return output

# ============================================================================
# 10. 三階 Alpha 生成（Trade When Operations）
# ============================================================================

def trade_when_factory(op, field, region):
    """三階 Alpha 工廠：以 trade_when(enter_event, alpha, exit_event) 包裝二階 Alpha。

    enter_event 涵蓋量價關係 / 波動率 / 極值 / 回歸四類事件（共 20 個）。
    exit_event：abs(returns) > 0.1（大幅波動出場）或 -1（永不出場）。
    輸出 20 × 2 = 40 個三階 Alpha。
    區域特定事件已定義但目前未啟用，可自行擴展。
    """
    output = []

    # 通用進場事件（20 個）：量價、波動、極值、回歸
    open_events = ["ts_arg_max(volume, 5) == 0", "ts_corr(close, volume, 20) < 0",
                   "ts_corr(close, volume, 5) < 0", "ts_mean(volume,10)>ts_mean(volume,60)",
                   "group_rank(ts_std_dev(returns,60), sector) > 0.7", "ts_zscore(returns,60) > 2",
                   "ts_arg_min(volume, 5) > 3",
                   "ts_std_dev(returns, 5) > ts_std_dev(returns, 20)",
                   "ts_arg_max(close, 5) == 0", "ts_arg_max(close, 20) == 0",
                   "ts_corr(close, volume, 5) > 0", "ts_corr(close, volume, 5) > 0.3",
                   "ts_corr(close, volume, 5) > 0.5",
                   "ts_corr(close, volume, 20) > 0", "ts_corr(close, volume, 20) > 0.3",
                   "ts_corr(close, volume, 20) > 0.5",
                   "ts_regression(returns, %s, 5, lag = 0, rettype = 2) > 0" % field,
                   "ts_regression(returns, %s, 20, lag = 0, rettype = 2) > 0" % field,
                   "ts_regression(returns, ts_step(20), 20, lag = 0, rettype = 2) > 0",
                   "ts_regression(returns, ts_step(5), 5, lag = 0, rettype = 2) > 0"]

    exit_events = ["abs(returns) > 0.1", "-1"]

    # 區域特定事件（已定義但目前未使用）
    usa_events = ["rank(rp_css_business) > 0.8", "ts_rank(rp_css_business, 22) > 0.8", ...]
    asi_events = [...]
    eur_events = [...]
    glb_events = [...]
    chn_events = [...]
    kor_events = [...]
    twn_events = [...]

    for oe in open_events:
        for ee in exit_events:
            alpha = "%s(%s, %s, %s)" % (op, oe, field, ee)
            output.append(alpha)
    return output

# ============================================================================
# 11. 提交檢查
# ============================================================================

def check_submission(s, alpha_bag, gold_bag, start):
    """批量檢查 Alpha 提交資格（IS Tests，特別是 Self Correlation）。

    Args:
        s: 已登入 session（呼叫方負責 login）
        alpha_bag: 待檢查 alpha_id 列表
        gold_bag: 通過清單（output）
        start: 從第幾個開始

    遇 401/403 直接拋 SessionExpiredError 中斷整個 batch。
    """
    depot = []

    for idx, g in enumerate(alpha_bag):
        if idx < start:
            continue
        if idx % 5 == 0:
            print(idx)

        pc = get_check_submission(s, g)

        if pc == "sleep":
            print(f"[{g}] session degraded — appending back to queue, sleep 100s")
            sleep(100)
            alpha_bag.append(g)
        elif pc != pc:  # NaN
            print("check self-correlation error")
            sleep(100)
            alpha_bag.append(g)
        elif pc == "fail":
            continue
        elif pc == "error":
            depot.append(g)
        else:
            print(g)
            gold_bag.append((g, pc))

    print("錯誤列表:", depot)
    return gold_bag


def get_full_8gate(s, alpha_id):
    """擴充版 /check：把 8 個 gate 全部 dict 出來，給 GUI Submission Readiness Inspector 用。

    回傳格式（dict）：
    {
        'alpha_id': str,
        'is_pass': bool,                    # IS-test 全 PASS
        'gates': [
            {'name': 'IS_SHARPE', 'value': float, 'result': 'PASS'/'FAIL'/'WARN', 'limit': float|None}, ...
        ],
        'self_correlation': float|None,     # 0~1
        'prod_correlation': float|None,     # 0~1（若有）
        'sub_universe_sharpe': float|None,
        'concentrated_weight': float|None,
        'iqc_delta': float|None,            # 從 BA-perf 拿
        'pool_corr_after': float|None,      # 從 BA-perf 拿
        'os_sharpe_ratio': float|None,      # os_sharpe / is_sharpe
        'error': str|None,
    }

    若任何 endpoint 401 → 拋 SessionExpiredError；其他錯只記在 error 不中斷。
    """
    out = {
        'alpha_id': alpha_id, 'is_pass': False, 'gates': [],
        'self_correlation': None, 'prod_correlation': None,
        'sub_universe_sharpe': None, 'concentrated_weight': None,
        'iqc_delta': None, 'pool_corr_after': None, 'os_sharpe_ratio': None,
        'error': None,
    }
    # 1) /check — 8 gate 全集合
    try:
        while True:
            r = s.get(f"{WQ_BASE_URL}/alphas/{alpha_id}/check", timeout=HTTP_TIMEOUT)
            _raise_if_unauthorized(r, f"GET /alphas/{alpha_id}/check")
            if not r.headers.get('Retry-After'):
                break
            time.sleep(float(r.headers['Retry-After']))
        body = r.json()
        if body.get('is', 0) == 0:
            out['error'] = 'session expired (is==0)'
            return out
        checks = body.get('is', {}).get('checks') or []
        out['gates'] = checks
        out['is_pass'] = not any((c.get('result') == 'FAIL') for c in checks)
        # 抽幾個關鍵 gate 出來方便 GUI 取用
        for c in checks:
            name = c.get('name', '').upper()
            val = c.get('value')
            if name == 'SELF_CORRELATION':
                out['self_correlation'] = val
            elif name == 'PROD_CORRELATION':
                out['prod_correlation'] = val
            elif name in ('LOW_SUB_UNIVERSE_SHARPE', 'SUB_UNIVERSE_SHARPE'):
                out['sub_universe_sharpe'] = val
            elif name in ('CONCENTRATED_WEIGHT', 'WEIGHT'):
                out['concentrated_weight'] = val
    except SessionExpiredError:
        raise
    except Exception as e:
        out['error'] = f'/check failed: {e}'

    # 2) /alphas/{id}/correlations/prod — 額外備援（若 /check 沒給）
    if out['prod_correlation'] is None:
        try:
            r = s.get(f"{WQ_BASE_URL}/alphas/{alpha_id}/correlations/prod",
                      timeout=HTTP_TIMEOUT)
            _raise_if_unauthorized(r, f"GET prod-corr {alpha_id}")
            if r.status_code == 200:
                body = r.json()
                # WQ 回 list of (id, value) 通常取 max
                if isinstance(body, list) and body:
                    vals = [x.get('value', 0) for x in body if isinstance(x, dict)]
                    if vals:
                        out['prod_correlation'] = max(vals)
                elif isinstance(body, dict) and 'max' in body:
                    out['prod_correlation'] = body.get('max')
        except SessionExpiredError:
            raise
        except Exception:
            pass

    # 3) /alphas/{id}/recordsets — OS sharpe ratio
    try:
        r = s.get(f"{WQ_BASE_URL}/alphas/{alpha_id}", timeout=HTTP_TIMEOUT)
        _raise_if_unauthorized(r, f"GET /alphas/{alpha_id}")
        if r.status_code == 200:
            body = r.json()
            is_sh = body.get('is', {}).get('sharpe') if isinstance(body.get('is'), dict) else None
            os_sh = body.get('os', {}).get('sharpe') if isinstance(body.get('os'), dict) else None
            if is_sh and os_sh and is_sh != 0:
                out['os_sharpe_ratio'] = float(os_sh) / float(is_sh)
    except SessionExpiredError:
        raise
    except Exception:
        pass

    # 4) BA-perf / IQC：之前就用過的兩個 endpoint，wrap 在 try 裡（很多帳號沒這權限）
    try:
        r = s.get(f"{WQ_BASE_URL}/users/self/alphas/{alpha_id}/before-and-after-performance",
                  timeout=HTTP_TIMEOUT)
        _raise_if_unauthorized(r, f"GET BA-perf {alpha_id}")
        if r.status_code == 200 and r.text.strip():
            body = r.json()
            stats = body.get('stats', {}) if isinstance(body, dict) else {}
            after = stats.get('after', {}) if isinstance(stats, dict) else {}
            out['pool_corr_after'] = after.get('correlation') if isinstance(after, dict) else None
    except SessionExpiredError:
        raise
    except Exception:
        pass

    try:
        r = s.get(f"{WQ_BASE_URL}/competitions/IQC2026S1/alphas/{alpha_id}/before-and-after-performance",
                  timeout=HTTP_TIMEOUT)
        _raise_if_unauthorized(r, f"GET IQC-perf {alpha_id}")
        if r.status_code == 200 and r.text.strip():
            body = r.json()
            out['iqc_delta'] = body.get('score') if isinstance(body, dict) else None
    except SessionExpiredError:
        raise
    except Exception:
        pass

    return out


def get_check_submission(s, alpha_id):
    """檢查單一 Alpha 提交資格，回傳 SELF_CORRELATION 值或狀態字串。

    回傳：
    - float → SELF_CORRELATION 值（< 0.7 為通過）
    - "sleep" → 會話過期（is == 0）
    - "fail" → 至少一項 IS Test 為 FAIL
    - "error" → 其他例外
    收到 Retry-After header 時等待指定秒數後重試。
    """
    while True:
        result = s.get("https://api.worldquantbrain.com/alphas/" + alpha_id + "/check",
                       timeout=HTTP_TIMEOUT)
        _raise_if_unauthorized(result, f"GET /alphas/{alpha_id}/check")
        if not result.headers.get("Retry-After"):
            break
        time.sleep(float(result.headers["Retry-After"]))
    try:
        if result.json().get("is", 0) == 0:
            print("logged out")
            return "sleep"

        checks_df = pd.DataFrame(result.json()["is"]["checks"])
        pc = checks_df[checks_df.name == "SELF_CORRELATION"]["value"].values[0]

        if not any(checks_df["result"] == "FAIL"):
            return pc
        else:
            return "fail"
    except:
        print("catch: %s" % (alpha_id))
        return "error"

# ============================================================================
# 12. Alpha 查看與定位
# ============================================================================

def view_alphas(s, gold_bag):
    """逐一定位 gold_bag 中的 Alpha 並按 Sharpe 降序印出。呼叫方需傳入已登入 session。"""
    sharp_list = []

    for gold, pc in gold_bag:
        triple = locate_alpha(s, gold)
        info = [triple[0], triple[2], triple[3], triple[4], triple[5], triple[6], triple[1]]
        info.append(pc)
        sharp_list.append(info)

    sharp_list.sort(reverse=True, key=lambda x: x[1])

    for i in sharp_list:
        print(i)


def locate_alpha(s, alpha_id):
    """取得單一 Alpha 完整資訊，回傳 [alpha_id, exp, sharpe, turnover, fitness, margin, dateCreated, decay]。

    收到 Retry-After 時等待後重試。
    """
    while True:
        alpha = s.get("https://api.worldquantbrain.com/alphas/" + alpha_id, timeout=HTTP_TIMEOUT)
        _raise_if_unauthorized(alpha, f"GET /alphas/{alpha_id}")
        if not alpha.headers.get("Retry-After"):
            break
        time.sleep(float(alpha.headers["Retry-After"]))

    string = alpha.content.decode('utf-8')
    metrics = json.loads(string)

    dateCreated = metrics["dateCreated"]
    sharpe = metrics["is"]["sharpe"]
    fitness = metrics["is"]["fitness"]
    turnover = metrics["is"]["turnover"]
    margin = metrics["is"]["margin"]
    decay = metrics["settings"]["decay"]
    exp = metrics['regular']['code']

    triple = [alpha_id, exp, sharpe, turnover, fitness, margin, dateCreated, decay]
    return triple

# ============================================================================
# 13. 顧問版本函數（Consultant，備用）
# ============================================================================

def multi_simulate(s, alpha_pools, neut, region, universe, start, max_workers: int = 4,
                   post_interval: float = 3.0):
    """Consultant 版本：每次提交 10 個 Alpha 批量回測，testPeriod='P2Y'。

    Args 同 single_simulate；alpha_pools 為三層嵌套（pools → tasks → alphas）。
    呼叫方需傳入已登入 session。
    """
    for x, pool in enumerate(alpha_pools):
        if x < start:
            continue

        progress_urls = []
        for y, task in enumerate(pool):
            if y > 0:
                sleep(post_interval)
            sim_data_list = generate_sim_data(task, region, universe, neut)
            try:
                resp = s.post('https://api.worldquantbrain.com/simulations',
                              json=sim_data_list, timeout=HTTP_TIMEOUT)
            except requests.exceptions.RequestException as e:
                print(f"[pool {x} task {y}] POST network error: {e}")
                continue

            _raise_if_unauthorized(resp, f"POST /simulations pool={x} task={y}")

            if resp.status_code == 429:
                wait = float(resp.headers.get("Retry-After", "60"))
                print(f"[pool {x} task {y}] rate-limited, sleep {wait:.0f}s")
                sleep(wait)
                continue

            loc = resp.headers.get('Location')
            if not loc:
                print(f"[pool {x} task {y}] no Location, status={resp.status_code} body={resp.text[:200]}")
                continue
            progress_urls.append(loc)

        print(f"pool {x} post done ({len(progress_urls)} submitted)")

        if not progress_urls:
            continue
        with ThreadPoolExecutor(max_workers=min(max_workers, len(progress_urls))) as ex:
            futures = {ex.submit(_poll_progress, s, url): url for url in progress_urls}
            for fut in as_completed(futures):
                url = futures[fut]
                try:
                    _, status, err = fut.result()
                except SessionExpiredError:
                    raise
                except Exception as e:
                    print(f"poll {url} crashed: {e}")
                    continue
                if status not in ("COMPLETE", "WARNING"):
                    print(f"Not complete [{status}]: {url} err={err}")

        print(f"pool {x} simulate done")

    print("Simulate done")


def generate_sim_data(alpha_list, region, uni, neut):
    """生成 multi_simulate 所需的批量模擬配置（testPeriod='P2Y'）。"""
    sim_data_list = []
    for alpha, decay in alpha_list:
        simulation_data = {
            'type': 'REGULAR',
            'settings': {
                'instrumentType': 'EQUITY',
                'region': region,
                'universe': uni,
                'delay': 1,
                'decay': decay,
                'neutralization': neut,
                'truncation': 0.08,
                'pasteurization': 'ON',
                'testPeriod': 'P2Y',
                'unitHandling': 'VERIFY',
                'nanHandling': 'ON',
                'language': 'FASTEXPR',
                'visualization': False,
            },
            'regular': alpha}
        sim_data_list.append(simulation_data)
    return sim_data_list


def load_task_pool(alpha_list, limit_of_children_simulations, limit_of_multi_simulations):
    """生成 Consultant 版本三層任務池：pools → tasks → alphas。

    每 task 含 limit_of_children_simulations 個 Alpha（通常 10）；
    每 pool 含 limit_of_multi_simulations 個 task（通常 10）。
    """
    tasks = [alpha_list[i:i + limit_of_children_simulations]
             for i in range(0, len(alpha_list), limit_of_children_simulations)]
    pools = [tasks[i:i + limit_of_multi_simulations]
             for i in range(0, len(tasks), limit_of_multi_simulations)]
    return pools

# ============================================================================
# 14. 其他輔助工廠
# ============================================================================

def vector_factory(op, field):
    """向量操作符工廠：對每個 vector（目前僅 'cap'）生成 op(field, vector)。"""
    output = []
    vectors = ["cap"]

    for vector in vectors:
        alpha = "%s(%s, %s)" % (op, field, vector)
        output.append(alpha)

    return output


def ts_comp_factory(op, field, factor, paras):
    """複合時間序列操作符工廠：時間窗口 [5,22,66,240] × paras 笛卡爾積。

    factor 為參數名（'percentage' / 'k' / 'buckets' 等），paras 為值列表；
    依 para 型別（float / int）格式化字串。
    """
    output = []
    l1, l2 = [5, 22, 66, 240], paras
    comb = list(product(l1, l2))

    for day, para in comb:
        if type(para) == float:
            alpha = "%s(%s, %d, %s=%.1f)" % (op, field, day, factor, para)
        elif type(para) == int:
            alpha = "%s(%s, %d, %s=%d)" % (op, field, day, factor, para)
        output.append(alpha)

    return output


def twin_field_factory(op, field, fields):
    """雙欄位工廠：對 (field × 其他欄位) 配對 × 時間窗口 [5,22,66,240] 生成表達式（如 ts_corr）。"""
    output = []
    days = [5, 22, 66, 240]
    outset = list(set(fields) - set([field]))

    for day in days:
        for counterpart in outset:
            alpha = "%s(%s, %s, %d)" % (op, field, counterpart, day)
            output.append(alpha)

    return output

# ============================================================================
# 15. 特殊登入：香港地區生物辨識
# ============================================================================

def login_hk():
    """香港地區生物辨識登入：若 401 + WWW-Authenticate=persona，提示使用者完成人臉掃描後重試直到 201。

    一般使用者請改用 login()。回傳 requests.Session。
    """
    username = ""
    password = ""

    s = requests.Session()
    s.auth = (username, password)

    response = s.post('https://api.worldquantbrain.com/authentication')

    if response.status_code == requests.codes.unauthorized:
        if response.headers.get("WWW-Authenticate") == "persona":
            print(
                "Complete biometrics authentication by scanning your face. Follow the link: \n"
                + urljoin(response.url, response.headers["Location"]) + "\n"
            )
            input("Press any key after you complete the biometrics authentication.")

            biometrics_response = s.post(urljoin(response.url, response.headers["Location"]))

            while biometrics_response.status_code != 201:
                input("Biometrics authentication is not complete. Please try again and press any key when completed.")
                biometrics_response = s.post(urljoin(response.url, response.headers["Location"]))

            print("Biometrics authentication completed.")
        else:
            print("\nIncorrect username or password. Please check your credentials.\n")
    else:
        print("Logged in successfully.")

    return s
