# mt5_pnl_to_wpx_loop.py
# 目的:
# - MT5(ウィジェット用: C:\MT5_Second\terminal64.exe) から口座成績を取得し performance.json を30秒ごとに更新
# - swap/commission を含めた数字を優先して計算（可能な範囲で）
# - ログはローテで肥大化させない

import os
import json
import time
import math
import datetime as dt
import traceback
from pathlib import Path
import logging
from logging.handlers import RotatingFileHandler

import MetaTrader5 as mt5


# =====================
# 設定
# =====================
TERMINAL_PATH = r"C:\MT5_Second\terminal64.exe"   # ★ウィジェット用MT5
OUTFILE = r"C:\fx_http\performance.json"

LOG_DIR = r"C:\fx_http\logs"
LOG_FILE = os.path.join(LOG_DIR, "mt5_pnl_to_wpx_loop.log")

REFRESH_SEC = 30

# 収益集計の開始日（あなたの運用開始日。必要ならここを変える）
START_DATE_JST = "2025-12-02"

# ログローテ
LOG_MAX_BYTES = 512 * 1024
LOG_BACKUP_COUNT = 5


# =====================
# ログ
# =====================
def setup_logger() -> logging.Logger:
    os.makedirs(LOG_DIR, exist_ok=True)
    logger = logging.getLogger("mt5_pnl_to_wpx_loop")
    logger.setLevel(logging.INFO)
    if logger.handlers:
        return logger

    h = RotatingFileHandler(
        LOG_FILE,
        maxBytes=LOG_MAX_BYTES,
        backupCount=LOG_BACKUP_COUNT,
        encoding="utf-8",
    )
    fmt = logging.Formatter("[%(asctime)s] %(levelname)s %(message)s")
    h.setFormatter(fmt)
    logger.addHandler(h)

    logger.info("start mt5_pnl_to_wpx_loop (robust)")
    logger.info("TERMINAL_PATH=%s", TERMINAL_PATH)
    logger.info("OUTFILE=%s", OUTFILE)
    logger.info("START_DATE_JST=%s", START_DATE_JST)
    return logger


log = setup_logger()


# =====================
# util
# =====================
def now_jst() -> dt.datetime:
    return dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc).astimezone(dt.timezone(dt.timedelta(hours=9)))


def jst_str(ts: dt.datetime) -> str:
    return ts.strftime("%Y-%m-%d %H:%M:%S")


def parse_start_dt_utc() -> dt.datetime:
    # JSTの日付を UTC に変換して MT5 API に渡す
    y, m, d = map(int, START_DATE_JST.split("-"))
    jst = dt.timezone(dt.timedelta(hours=9))
    start_jst = dt.datetime(y, m, d, 0, 0, 0, tzinfo=jst)
    return start_jst.astimezone(dt.timezone.utc).replace(tzinfo=None)


def safe_float(x):
    try:
        if x is None:
            return 0.0
        v = float(x)
        if math.isfinite(v):
            return v
        return 0.0
    except Exception:
        return 0.0


def mt5_connect() -> bool:
    mt5.shutdown()
    ok = mt5.initialize(path=TERMINAL_PATH)
    if not ok:
        log.error("mt5.initialize failed: %s", mt5.last_error())
        return False
    return True


def write_json(payload: dict) -> None:
    Path(OUTFILE).write_text(json.dumps(payload, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")


# =====================
# 成績計算
# =====================
def calc_realized_from_deals(start_utc_naive: dt.datetime) -> tuple[float, int, int, int, float, float]:
    """
    実現損益: deals の profit + swap + commission を合算（取れる範囲で）
    戻り:
      realized_pnl, closed_count, win_count, lose_count, max_win, max_lose
    """
    deals = mt5.history_deals_get(start_utc_naive, dt.datetime.utcnow())
    if deals is None:
        return 0.0, 0, 0, 0, 0.0, 0.0

    realized = 0.0
    closed_count = 0
    win = 0
    lose = 0
    max_win = 0.0
    max_lose = 0.0

    for d in deals:
        # 「決済」だけを数える（entry が OUT/OUT_BY のもの）
        entry = getattr(d, "entry", None)
        if entry not in (mt5.DEAL_ENTRY_OUT, mt5.DEAL_ENTRY_OUT_BY):
            continue

        profit = safe_float(getattr(d, "profit", 0.0))
        swap = safe_float(getattr(d, "swap", 0.0))
        commission = safe_float(getattr(d, "commission", 0.0))
        v = profit + swap + commission

        realized += v
        closed_count += 1

        if v > 0:
            win += 1
            if v > max_win:
                max_win = v
        elif v < 0:
            lose += 1
            if abs(v) > abs(max_lose):
                max_lose = v  # マイナスのまま保持

    return realized, closed_count, win, lose, max_win, max_lose


def calc_floating_from_positions() -> float:
    """
    含み損益:
    position.profit に swap/commission が含まれるかは口座/ブローカー実装差があるので、
    利用できる属性があれば足す（安全に）。
    """
    pos = mt5.positions_get()
    if pos is None:
        return 0.0

    floating = 0.0
    for p in pos:
        profit = safe_float(getattr(p, "profit", 0.0))
        swap = safe_float(getattr(p, "swap", 0.0))
        commission = safe_float(getattr(p, "commission", 0.0))
        floating += (profit + swap + commission)

    return floating


def main():
    start_utc = parse_start_dt_utc()

    while True:
        ts = now_jst()
        updated_at = jst_str(ts)

        try:
            if not mt5_connect():
                payload = {
                    "updated_at": updated_at,
                    "status": "down",
                    "status_text": "停止中(市場/接続)",
                    "start_date_jst": START_DATE_JST,
                    "realized_pnl": 0,
                    "floating_pnl": 0,
                    "total_pnl": 0,
                    "closed_count": 0,
                    "win_count": 0,
                    "lose_count": 0,
                    "win_rate": 0.0,
                    "max_win": 0,
                    "max_lose": 0,
                }
                write_json(payload)
                log.info("saved: %s status=down (mt5 init fail)", OUTFILE)
                time.sleep(REFRESH_SEC)
                continue

            realized, closed_count, win_count, lose_count, max_win, max_lose = calc_realized_from_deals(start_utc)
            floating = calc_floating_from_positions()
            total = realized + floating

            win_rate = 0.0
            if closed_count > 0:
                win_rate = (win_count / closed_count) * 100.0

            payload = {
                "updated_at": updated_at,
                "status": "live",
                "start_date_jst": START_DATE_JST,
                "realized_pnl": int(round(realized)),
                "floating_pnl": int(round(floating)),
                "total_pnl": int(round(total)),
                "closed_count": int(closed_count),
                "win_count": int(win_count),
                "lose_count": int(lose_count),
                "win_rate": float(round(win_rate, 1)),
                "max_win": int(round(max_win)),
                "max_lose": int(round(max_lose)),  # マイナスならマイナスのまま
            }

            write_json(payload)
            log.info("saved: %s", OUTFILE)

        except Exception as e:
            log.error("loop error: %s", e)
            log.error(traceback.format_exc())
        finally:
            try:
                mt5.shutdown()
            except Exception:
                pass

        time.sleep(REFRESH_SEC)


if __name__ == "__main__":
    main()
