#!/usr/bin/env python3
"""持续实盘模拟盘: 初始10万, 最多持10只, 只买强信号(强度>=70)
- 每天17:00 cron调用: 读状态文件→卖出检查→今日信号(强度>=70)等权买入→净值落盘
- 状态文件: /Users/xpresso/zt_app/sim_trading_state.json (首次运行自动初始化)
- 买入=信号日(买入日bd)开盘x1.001滑点+买佣万2.5; 卖出按各池SELL_COMBO, 卖滑点x0.999+卖佣万2.5+印花千1
- 信号来源: strength_baseline.csv 中 buy_date==最新交易日的行(scan_daily_tb.py写入), std>=70
"""
import pandas as pd
import numpy as np
import sys, os, json, csv
sys.path.insert(0, '/Users/xpresso/zt_app/code')
import grid_or as g
from scan_daily_tb import COMBO, SELL_COMBO, simulate_sell, PN

BASE = g.BASE
QUANT_DIR = '/Users/xpresso/zt_app'
STATE_FILE = f'{QUANT_DIR}/sim_trading_state.json'
NAV_CSV = f'{QUANT_DIR}/sim_trading_nav.csv'
INIT = 100000.0
MAX_POS = 15          # 2026-09-09 参数定稿: 70/10 -> 85/15 (网格60窗口: θ85×N15-20为质量×频率×稳健平衡点)
MIN_STRENGTH = 85
SLIP = 0.001
C_BUY = 0.00025
C_SELL = 0.00025 + 0.001

# ---------- 强度计算(与sim_live.py同口径) ----------
model = json.load(open(f'{BASE}/strength_model.json'))
w = model['weights']; pools_m = model['pools']

def pct(pool, fc, v):
    qs = pools_m[pool]['percentiles'][fc]
    if qs is None or v is None or (isinstance(v, float) and np.isnan(v)):
        return 0.5
    return float(np.searchsorted(qs, v) / 100.0)

def strength_of(pool, fd):
    raw = sum(w[fc] * pct(pool, fc, fd[fc]) for fc in w)
    rs = pools_m[pool]['raw_scores']
    return float(np.searchsorted(rs, raw) / len(rs) * 100)

from plot_ideal_top10 import chain_for_buy
from build_strength_model import feats   # 唯一构建器(裁定 §4.6)

cache = {}
for pool in COMBO:
    _, dailies = g.load_pool(pool)
    cache[pool] = dailies

def latest_date():
    ds = set()
    for pool in COMBO:
        for code, d in cache[pool].items():
            ds |= set(d['trade_date'].values)
    return sorted(ds)[-1]

def load_state():
    if os.path.exists(STATE_FILE):
        return json.load(open(STATE_FILE))
    return {'cash': INIT, 'positions': {}, 'closed': [], 'nav_history': [], 'last_date': None, 'init_date': None}

def save_state(st):
    with open(STATE_FILE, 'w') as f:
        json.dump(st, f, ensure_ascii=False)

def pos_value_on(date, positions):
    val = 0.0
    for code, pos in positions.items():
        d = cache[pos['pool']][pos['code']].sort_values('trade_date').reset_index(drop=True)
        rows = d[d['trade_date'] == date]
        if len(rows) == 0:
            continue
        px = float(rows.iloc[0]['raw_close'])          # 估值=原始价
        val += pos['shares'] * px * (1 - SLIP) * (1 - C_SELL)
    return val

def main(today=None):
    today = today or latest_date()
    st = load_state()

    # 首次运行: 初始化起点
    if st['last_date'] is None:
        st['init_date'] = today
        st['last_date'] = today
        save_state(st)
        print(f"模拟盘初始化: {today} 初始{INIT/1e4:.0f}万 空仓 (首个交易日)")
        # 初始化: 表头 + 初始本金行(两端定稿: nav 含起始行, 行尾 LF; 与 research 侧同构)
        with open(NAV_CSV, 'w', newline='') as f:
            w = csv.writer(f, lineterminator='\n')
            w.writerow(['date', 'cash', 'pos_value', 'n_pos', 'total'])
            w.writerow([today, f'{INIT:.2f}', '0.00', 0, f'{INIT:.2f}'])
        return

    if st['last_date'] >= today:
        print(f"模拟盘已更新至{st['last_date']}, 今日{today}无新数据, 跳过")
        return

    st['last_date'] = today
    cash = st['cash']
    positions = st['positions']
    ops_buy, ops_sell = [], []
    # ---- 资金时序修正(2026-09-10): 快照昨收结转现金, 卖出按收盘成交回款当日不可用 ----
    cash_at_open = cash  # 开盘时点真实可用现金(今日卖出回款尚不可用)
    n_pos_at_open = len(positions)  # 开盘时点真实持仓数(今日收盘卖出腾出的槽位当日不可用)

    # 1. 卖出检查(用截止今天的完整数据模拟)
    for code in list(positions.keys()):
        pos = positions[code]
        d = cache[pos['pool']][pos['code']].sort_values('trade_date').reset_index(drop=True)
        d_cut = d[d['trade_date'] <= today].reset_index(drop=True)
        if len(d_cut) == 0 or d_cut['trade_date'].iloc[-1] != today:
            continue
        res = simulate_sell(d_cut, {'code': pos['code'], 'buy_date': pos['buy_date']}, SELL_COMBO[pos['pool']])
        if res is not None and res['sell_date'] == today:
            px = res['sell_price']
            proceeds = pos['shares'] * px * (1 - SLIP) * (1 - C_SELL)
            cash += proceeds
            pnl = proceeds - pos['spent']
            st['closed'].append({**pos, 'sell_date': today, 'sell_px': px, 'reason': res['reason'],
                                 'pnl': pnl, 'ret_net': res['ret_net']})
            ops_sell.append(f"{pos['name'] if 'name' in pos else pos['code']}({pos['code'][:8]}) {res['reason']} {res['ret_net']*100:+.1f}%")
            del positions[code]

    # 2. 今日买入: 从baseline读 buy_date==today 的信号 (只能用 cash_at_open)
    bdf = pd.read_csv(f'{BASE}/strength_baseline.csv', dtype={'buy_date': str})
    sigs_today = bdf[bdf['buy_date'] == today].copy()
    cands = []
    for _, r in sigs_today.iterrows():
        pool = r['pool']
        code = r['code']
        if code in positions:
            continue
        # 强度: baseline里存的std列? 若无则重算(保险: 用baseline的std列)
        std = float(r['strength'])
        if std >= MIN_STRENGTH:
            cands.append((std, pool, code))
    cands.sort(key=lambda x: -x[0])
    # 按code去重(保留强度最高)
    dedup = {}
    for std, pool, code in cands:
        if code not in dedup:
            dedup[code] = (std, pool, code)
    cands = list(dedup.values())
    slots = MAX_POS - n_pos_at_open  # 槽位按开盘时点持仓数算(当日卖出腾出的槽位不可用)
    take = cands[:slots]
    if take and cash_at_open > 0:
        # 2026-09-10 用户定稿: 按槽位摊资金(每仓位目标=现金/槽位), 而非按批内信号数平摊。
        # 例: 持仓5/15, 达标信号3只 -> 每只用 现金/10, 只动用3/10资金, 其余留给后续强信号
        per = cash_at_open / slots
        for std, pool, code in take:
            if code in positions:
                continue
            d = cache[pool][code].sort_values('trade_date').reset_index(drop=True)
            rows = d[d['trade_date'] == today]
            if len(rows) == 0:
                continue
            buy_px = float(rows.iloc[0]['raw_open']) * (1 + SLIP) * (1 + C_BUY)   # 成交价=原始价
            if buy_px <= 0:
                continue
            shares = int(per / buy_px)
            if shares < 1:
                continue
            spent = shares * buy_px
            cash -= spent
            cash_at_open -= spent
            name = g.stock_name(code) if hasattr(g, 'stock_name') else code
            positions[code] = {'pool': pool, 'code': code, 'buy_date': today, 'buy_px': buy_px,
                               'shares': shares, 'spent': spent, 'std': std, 'name': name}
            ops_buy.append(f"{name}({code[:8]}) 强度{std:.0f} @{buy_px:.2f}")

    # 3. 净值
    pv = pos_value_on(today, positions)
    total = cash + pv
    st['cash'] = cash
    st['positions'] = positions
    st['params'] = {'max_pos': MAX_POS, 'min_strength': MIN_STRENGTH}
    st['nav_history'].append({'date': today, 'nav': total, 'n_pos': len(positions)})
    save_state(st)
    with open(NAV_CSV, 'a', newline='') as f:
        csv.writer(f, lineterminator='\n').writerow([today, f'{cash:.2f}', f'{pv:.2f}', len(positions), f'{total:.2f}'])

    ret = total / INIT - 1
    print(f"===== 模拟盘更新 {today} =====")
    print(f"总资产: {total:,.0f}元 (初始10万, 累计{ret*100:+.1f}%) | 现金{cash:,.0f} | 持仓{len(positions)}只")
    if ops_buy:
        print(f"今日买入{len(ops_buy)}只: " + "; ".join(ops_buy))
    else:
        print("今日买入: 无(无强度>=70新信号或持仓已满)")
    if ops_sell:
        print(f"今日卖出{len(ops_sell)}只: " + "; ".join(ops_sell))
    else:
        print("今日卖出: 无")
    if positions:
        print("当前持仓:")
        for code, pos in positions.items():
            d = cache[pos['pool']][pos['code']].sort_values('trade_date').reset_index(drop=True)
            cur = float(d.iloc[-1]['raw_close'])
            r = cur / pos['buy_px'] - 1
            print(f"  {pos.get('name', code)}({code[:8]}) 买{pos['buy_date']}@{pos['buy_px']:.2f} 现{cur:.2f} 浮盈{r*100:+.1f}% 强度{pos['std']:.0f}")
    n_closed = len(st['closed'])
    wins = sum(1 for c in st['closed'] if c['pnl'] > 0)
    if n_closed:
        print(f"已平仓{n_closed}笔 胜率{wins/n_closed*100:.0f}%")

if __name__ == '__main__':
    main()
