
    ^j+L                       d Z ddlmZ ddlZddlZddlZddlZddlmZm	Z	m
Z
 ddlmZ ddlmZ ddlmZ ddlmZmZmZ d	Ze	 G d
 d             Ze	 G d d             Ze	 G d d             Z e	d       G d d             Ze	 G d d             Ze	 G d d             Ze	 G d d             Ze	 G d d             Zd&dZd&dZd&dZ  G d d       Z! G d! d"e"e      Z#e#jH                  d#d$	 	 	 	 	 	 	 d'd%Z%y)(u  
APEX V16 — State persistence.

Single point of truth for saving/loading bot state.
Replaces V15's opaque v15_state.json with:
  - Schema versioning (explicit migration on schema change)
  - Pre-save backup (.prev.json kept always)
  - File scoped per (mode, account) — never mix paper with live
  - Atomic write (write temp + fsync + rename + dir-fsync)
  - load_state(store, mode) helper with FRESH timestamped archive

Schema v2 (current) — sub-dataclasses, replaces v1 flat layout:
  - active_trades: dict[symbol -> ActiveTrade(entry, runtime)]
  - daily:        DailyCounters (per-day P&L, halt flags, counts; UTC date)
  - brain:        BrainCounters (per-Brain wins/losses + bias_calls_count)
  - bias_cache:   BiasCache (per-symbol BiasEntry frozen snapshots)
  - cooldown:     CooldownState (per-symbol last_close + cooldown_until)
  - session_pnl, halted, halt_reason, metadata at top level
    )annotationsN)asdict	dataclassfield)Enum)Path)Optional)
TradeEntryTradeRuntimeutc_now   c                  &    e Zd ZU dZded<   ded<   y)ActiveTradez:Pair of TradeEntry (immutable) and TradeRuntime (mutable).r
   entryr   runtimeN)__name__
__module____qualname____doc____annotations__     ./home/work/apex_v16/persistence/state_store.pyr   r   *   s    Dr   r   c                      e Zd ZU dZ ed       Zded<   dZded<   d	Zd
ed<   d	Z	d
ed<   dZ
ded<   dZded<   dZded<   ddZedd       Zy)DailyCountersu  
    Per-day counters. Reset at UTC midnight (auto when load_state runs
    with auto_daily_reset=True and state.daily.date != today UTC).

    daily_loss_hard_stop_hit and profit_target_hit are explicit V16 flags.
    V15 collapsed both into a single `halted` kill switch — V16 separates
    them so RiskManager can decouple "halted (no entries)" from
    "profit target reached (allow risk-reduced trades)" without
    recomputing from daily_pnl at runtime.
    c                 N    t               j                         j                         S N)r   date	isoformatr   r   r   <lambda>zDailyCounters.<lambda>=   s    ginn.>.H.H.J r   default_factorystrr           float	daily_pnlFbooldaily_loss_hard_stop_hitprofit_target_hitr   intapproved_countexecuted_countrejected_countc                    t        |       S r   r   selfs    r   to_dictzDailyCounters.to_dictE       d|r   c                H    | |j                  dt               j                         j                               |j                  dd      |j                  dd      |j                  dd      |j                  dd      |j                  d	d      |j                  d
d            S )Nr   r&   r$   r(   Fr)   r+   r   r,   r-   r   r&   r(   r)   r+   r,   r-   getr   r   r   clsdatas     r   	from_dictzDailyCounters.from_dictH   s    &').."2"<"<">?hh{C0%)XX.H%%P"hh':EB88$4a888$4a888$4a8
 	
r   Nreturndict)r:   r>   r=   z'DailyCounters')r   r   r   r   r   r   r   r&   r(   r)   r+   r,   r-   r2   classmethodr;   r   r   r   r   r   1   sp    	 &JKD#KIu%*d*#t#NCNCNC 	
 	
r   r   c                  r    e Zd ZU dZdZded<   dZded<   dZded<   dZded<   dZ	ded<   dd	Z
edd
       Zy)BrainCountersz
    Per-Brain win/loss counters + AI cost analytics.

    bias_calls_count is V16-new: V15 did not persist AI-bias call counts
    (only an in-memory cache). Tracked here for Anthropic per-token cost
    analytics across sessions.
    r   r*   mr_wins	mr_lossestf_wins	tf_lossesbias_calls_countc                    t        |       S r   r/   r0   s    r   r2   zBrainCounters.to_dictd   r3   r   c           
          | |j                  dd      |j                  dd      |j                  dd      |j                  dd      |j                  dd            S )NrB   r   rC   rD   rE   rF   rB   rC   rD   rE   rF   r7   r8   s     r   r;   zBrainCounters.from_dictg   sV    HHY*hh{A.HHY*hh{A.!XX&8!<
 	
r   Nr<   )r:   r>   r=   z'BrainCounters')r   r   r   r   rB   r   rC   rD   rE   rF   r2   r?   r;   r   r   r   rA   rA   U   sU     GSIsGSIsc 
 
r   rA   T)frozenc                  T    e Zd ZU dZded<   ded<   ded<   ded<   ddZedd	       Zy
)	BiasEntryu  
    Frozen snapshot of an AI bias decision at computed_at.

    The state store persists the snapshot only — it does NOT enforce
    freshness. The consumer (BrainBias) MUST check (utc_now - computed_at)
    against its freshness policy (e.g. >1h → recompute) before using a
    cached entry. Bias is time-sensitive: a 4-hour-old bias on a
    fast-moving session is misleading.
    r#   	directionr*   
confidence	rationalecomputed_atc                    t        |       S r   r/   r0   s    r   r2   zBiasEntry.to_dict   r3   r   c                L     | |d   |d   |j                  dd      |d         S )NrN   rO   rP    rQ   )rN   rO   rP   rQ   rJ   r8   s     r   r;   zBiasEntry.from_dict   s5    ;'L)hh{B/]+	
 	
r   Nr<   )r:   r>   r=   z'BiasEntry')r   r   r   r   r   r2   r?   r;   r   r   r   rM   rM   r   s7     NON 
 
r   rM   c                  H    e Zd ZU dZ ee      Zded<   ddZe	d	d       Z
y)
	BiasCachez>Per-symbol cache of frozen BiasEntry snapshots. See BiasEntry.r!   zdict[str, BiasEntry]entriesc                    d| j                   j                         D ci c]  \  }}||j                          c}}iS c c}}w )NrW   )rW   itemsr2   )r1   ses      r   r2   zBiasCache.to_dict   s6    t||7I7I7KLtq!Aqyy{NLMMLs   ?c           
         |j                  di       } | |j                         D ci c]  \  }}|t        j                  |       c}}      S c c}}w )NrW   )rW   )r7   rY   rM   r;   )r9   r:   rawrZ   r[   s        r   r;   zBiasCache.from_dict   sE    hhy"%#))+N$!QAy22155NOONs    A
Nr<   )r:   r>   r=   z'BiasCache')r   r   r   r   r   r>   rW   r   r2   r?   r;   r   r   r   rV   rV      s1    H$)$$?G!?N P Pr   rV   c                  H    e Zd ZU dZ ee      Zded<   ddZe	d	d       Z
y)
EntryEvalCacheu  
    Per-symbol last AI-entry-evaluated M5 candle time (UTC unix seconds).

    Used by Brain.evaluate_entry to skip the AI call when it has
    already been made on the current M5 candle for this symbol.
    Persists across restart — same-candle dedup survives crashes.

    Update timing (orchestrator): set last_eval[symbol] = candle_time
    when EntryEvalResult.evaluated_candle_time is populated. The brain
    populates that only when the AI either responded (any parse
    outcome) or returned a permanent error (credit/invalid). Transient
    AI failures (unknown/overload) leave evaluated_candle_time=None
    so the next iteration retries.
    r!   zdict[str, float]	last_evalc                0    dt        | j                        iS )Nr`   )r>   r`   r0   s    r   r2   zEntryEvalCache.to_dict   s    T$..122r   c                F     | t        |j                  di                   S )Nr`   )r`   r>   r7   r8   s     r   r;   zEntryEvalCache.from_dict   s    T$((;";<==r   Nr<   )r:   r>   r=   z'EntryEvalCache')r   r   r   r   r   r>   r`   r   r2   r?   r;   r   r   r   r_   r_      s2     #("=I=3 > >r   r_   c                  d    e Zd ZU dZ ee      Zded<    ee      Zded<   d	dZ	e
d
d       Zy)CooldownStatez
    Per-symbol post-trade cooldown tracking.
      last_trade_close_at: ISO datetime of most recent close per symbol.
      cooldown_until: optional explicit cooldown expiry per symbol
                      (e.g. enforced after consecutive losses).
    r!   zdict[str, str]last_trade_close_atcooldown_untilc                    t        |       S r   r/   r0   s    r   r2   zCooldownState.to_dict   r3   r   c           	     z     | t        |j                  di             t        |j                  di                   S )Nrf   rg   rf   rg   rc   r8   s     r   r;   zCooldownState.from_dict   s7     $TXX.CR%H I)92 >?
 	
r   Nr<   )r:   r>   r=   z'CooldownState')r   r   r   r   r   r>   rf   r   rg   r2   r?   r;   r   r   r   re   re      sA     +0*EE%*4%@NN@ 
 
r   re   c                  d   e Zd ZU dZeZded<    ed       Zded<    ed       Z	ded	<    ee
      Zd
ed<    ee      Zded<    ee      Zded<    ee      Zded<    ee      Zded<    ee      Zded<   dZded<   dZded<   dZded<    ee
      Zded<   d#d Zed$d!       Zy")%SessionStatez
    Full bot state, saved atomically to disk.
    Schema is versioned; bump SCHEMA_VERSION on breaking changes
    and add migration path in _migrate().
    r*   schema_versionc                 2    t               j                         S r   r   r   r   r   r   r    zSessionState.<lambda>   s    GI4G4G4I r   r!   r#   
started_atc                 2    t               j                         S r   ro   r   r   r   r    zSessionState.<lambda>   s    ')2E2E2G r   saved_atzdict[str, ActiveTrade]active_tradesr   dailyrA   brainrV   
bias_cacher_   entry_eval_cachere   cooldownr$   r%   session_pnlFr'   haltedrT   halt_reasonr>   metadatac                h   | j                   | j                  | j                  | j                  j	                         D ci c];  \  }}||j
                  j                         |j                  j                         d= c}}| j                  j                         | j                  j                         | j                  j                         | j                  j                         | j                  j                         | j                  | j                  | j                  | j                   dS c c}}w )Nr   r   rm   rp   rr   rs   rt   ru   rv   rw   rx   ry   rz   r{   r|   )rm   rp   rr   rs   rY   r   r2   r   rt   ru   rv   rw   rx   ry   rz   r{   r|   )r1   symbolats      r   r2   zSessionState.to_dict   s    "11// #'"4"4":":"<
 FB	 XX--/!zz113  ZZ'')ZZ'')//113 $ 5 5 = = ?--/++kk++'
 	
s   A D.c                   t        |      }i }|j                  di       j                         D ]A  \  }}t        t	        j
                  |d         t        j
                  |d               ||<   C  | |j                  dt              |j                  dt               j                               |j                  dt               j                               |t        j                  |j                  di             t        j                  |j                  d	i             t        j                  |j                  d
i             t        j                  |j                  di             t        j                  |j                  di             |j                  dd      |j                  dd      |j                  dd      |j                  di             S )Nrs   r   r   r~   rm   rp   rr   rt   ru   rv   rw   rx   ry   r$   rz   Fr{   rT   r|   r   )_migrater7   rY   r   r
   r;   r   SCHEMA_VERSIONr   r   r   rA   rV   r_   re   )r9   r:   rs   r   at_datas        r   r;   zSessionState.from_dict	  sv   ~#xx<BBD 	OFG$/ **77+;<$..wy/AB%M&!	 88$4nExxgi.A.A.CDXXj')*=*=*?@'))$((7B*?@))$((7B*?@ **488L"+EF+55dhh?QSU6VW",,TXXj"-EF488He,3XXj"-
 	
r   Nr<   )r:   r>   r=   z'SessionState')r   r   r   r   r   rm   r   r   rp   rr   r>   rs   r   rt   rA   ru   rV   rv   r_   rw   re   rx   ry   rz   r{   r|   r2   r?   r;   r   r   r   rl   rl      s    
 )NC(,IJJJ*GHHcH -2$,GM)G !?E=? ?E=?!)<J	<',^'LnL#MBHmB KFDK 40Hd0
. 
 
r   rl   c                   d| j                  d      | j                  d      | j                  di       | j                  dt               j                         j                               | j                  dd      dd| j                  d	d
      | j                  dd
      | j                  dd
      d| j                  dd
      | j                  dd
      | j                  dd
      | j                  dd
      d
ddi ii i d| j                  dd      | j                  dd      | j                  dd      | j                  di       dS )u>  
    Schema v1 (flat) → v2 (nested sub-dataclasses).

    Field mapping:
      approved_count, executed_count, rejected_count → daily.*
      daily_pnl, daily_pnl_date                      → daily.daily_pnl, daily.date
      mr_wins, mr_losses, tf_wins, tf_losses         → brain.*
      session_pnl, halted, halt_reason, metadata     → unchanged (top-level)
      active_trades                                  → unchanged

    daily_loss_hard_stop_hit / profit_target_hit / bias_calls_count are
    new in v2 — defaulted to False/0 for migrated v1 data.
       rp   rr   rs   daily_pnl_dater&   r$   Fr+   r   r,   r-   r5   rB   rC   rD   rE   rI   rW   rj   ry   rz   r{   rT   r|   )rm   rp   rr   rs   rt   ru   rv   rx   ry   rz   r{   r|   r6   )r:   s    r   _migrate_v1_to_v2r   )  s)    hh|,HHZ(/26HH-wy~~/?/I/I/KL+s3(-!&"hh'7;"hh'7;"hh'7;
 xx	1-+q1xx	1-+q1 !
 !"o,."Exxs3((8U+xxr2HHZ,5 r   c                4    t        |       }d|d<   di i|d<   |S )z
    Schema v2 -> v3: add entry_eval_cache (per-symbol same-candle dedup
    for evaluate_entry AI calls). Migration is purely additive: existing
    fields untouched, new field defaulted to empty cache.
    r   rm   r`   rw   )r>   )r:   outs     r   _migrate_v2_to_v3r   U  s-     t*CC*B/CJr   c                   | j                  dd      }|t        k(  r| S |t        kD  rt        d| dt         d      |dk(  rt        |       } d}|dk(  rt	        |       } d}|t        k(  r| S t        d	| d
t         d      )z
    Migrate older state schemas to current SCHEMA_VERSION.
    Loud failure on unknown schema (better than silent corruption).
    rm   r   zState file schema_version=z* is newer than this code's SCHEMA_VERSION=z(. Refusing to load (would corrupt data).   r   r   z+Unknown migration path from schema_version=z to z#. Add migration step in _migrate().)r7   r   
ValueErrorr   r   )r:   versions     r   r   r   a  s    
 hh'+G. (	 2**8)9 :56
 	
 !| &!| &. 

5gY ?@	B r   c                  @    e Zd ZdZd	dZd
dZddZddZddZddZ	y)
StateStorez
    Atomic file-based state persistence.

    Usage:
        store = StateStore(config.state_file)
        state = load_state(store, mode=StateLoadMode.RESUME, auto_daily_reset=True)
        # ... mutate state ...
        store.save(state)
    c                d    t        |      | _        | j                  j                  d      | _        y )Nz
.prev.json)r   
state_filewith_suffixbackup_file)r1   r   s     r   __init__zStateStore.__init__  s%    z*??66|Dr   c           
     |   | j                   j                         sy	 | j                   j                  dd      5 }t        j                  |      }ddd       t        j                        S # 1 sw Y   xY w# t        j
                  $ r/}t        d| j                    d| d| j                   d      |d}~ww xY w)	z
        Load state from disk. Returns None if file doesn't exist.
        Raises on corrupt file or unknown schema (loud failure).
        Nrutf-8encodingzState file z is corrupt: z. Backup at z may be usable.)
r   existsopenjsonloadJSONDecodeErrorRuntimeErrorr   rl   r;   )r1   fr:   r[   s       r   r   zStateStore.load  s    
 %%'	%%cG%< $yy|$ %%d++$ $## 	doo.mA3 ?!--.o? 	s.   A9 A-A9 -A62A9 9B;*B66B;c                >    | j                         }||S t               S )z=Load state, or return a fresh SessionState if no file exists.)r   rl   )r1   loadeds     r   load_or_freshzStateStore.load_or_fresh  s    +v??r   c                   t               j                         |_        | j                  j	                         r*t        j                  | j                  | j                         | j                  j                  j                  dd       t        j                  | j                  j                  dz   dt        | j                  j                              \  }}	 t        |dd      5 }t        j                   |j#                         |d	d
       |j%                          t'        j(                  |j+                                ddd       t-        |      j/                  | j                         	 t'        j                  t        | j                  j                        t&        j0                        }	 t'        j(                  |       t'        j2                  |       y# 1 sw Y   xY w# t'        j2                  |       w xY w# t4        $ r Y yw xY w# t6        $ r- 	 t-        |      j9                  d        # t6        $ r Y  w xY ww xY w)a  
        Atomic durable save:
          1. If state_file exists, copy it to backup_file (.prev.json)
          2. Write new state to temp file (same dir for atomic rename)
          3. flush + fsync the temp file (durable on disk)
          4. rename temp -> state_file (atomic on POSIX)
          5. fsync the parent directory (durable rename)

        Updates state.saved_at timestamp before serialization.
        T)parentsexist_ok.z.tmp)prefixsuffixdirwr   r   r   F)indentensure_asciiN
missing_ok)r   r   rr   r   r   shutilcopy2r   parentmkdirtempfilemkstempstemr#   r   r   dumpr2   flushosfsyncfilenor   replaceO_RDONLYcloseOSError	Exceptionunlink)r1   statefdtmp_pathr   dir_fds         r   savezStateStore.save  s    !,,.??!!#LL$*:*:;$$TD$A''??''#-DOO**+
H
	b#0 %A		%--/1QUK	$%
 N""4??3T__%;%;!<bkkJ%HHV$HHV$% % HHV$   	X%%%6   	s   H 'AG,H /AH 1G( H G%!H (G??H 	HH HH 	IH76I7	I IIIc                t    | j                   j                  d       | j                  j                  d       y)z;Delete state file and .prev backup. Use with --fresh-start.Tr   N)r   r   r   r0   s    r   resetzStateStore.reset  s.    $/40r   c                6    | j                   j                         S r   )r   r   r0   s    r   r   zStateStore.exists  s    %%''r   N)r   r   r=   None)r=   zOptional[SessionState])r=   rl   )r   rl   r=   r   )r=   r   )r=   r'   )
r   r   r   r   r   r   r   r   r   r   r   r   r   r   r     s(    E,&@.h1
(r   r   c                      e Zd ZdZdZdZy)StateLoadModez0How load_state should treat existing state file.RESUMEFRESHN)r   r   r   r   r   r   r   r   r   r   r     s    :FEr   r   F)modeauto_daily_resetc               p   |t         j                  k(  r| j                  j                         rt	               j                  d      }| j                  j                  d| d      }t        j                  | j                  |       | j                  j                          | j                  j                  d       t               S | j                         }|r^t	               j                         j                         }|j                  j                  |k7  rt!        |      |_        d|_        d|_        |S )	uW  
    Load state with explicit mode and optional daily reset.

    Modes:
      RESUME — Load existing state. Fresh SessionState if file missing.
      FRESH  — If state file exists, archive it to
               <state_file>.deleted-<UTC timestamp>.json then drop it
               (and the .prev backup). Return a fresh SessionState.
               Caller is responsible for the first save().

    auto_daily_reset:
      When True (typically LIVE runs), if state.daily.date != today UTC
      reset DailyCounters to a fresh one for today AND clear halted/halt_reason
      (halts are daily-scoped). session_pnl and active_trades are preserved
      (active_trades survive across days; closure logic resets them).
      V15 parity: V15 reset daily counters at UTC midnight via the
      session loop. V16 makes the reset declarative at load time.
    z%Y%m%dT%H%M%SZz	.deleted-z.jsonTr   )r   FrT   )r   r   r   r   r   strftimer   r   r   r   r   rl   r   r   r   rt   r   rz   r{   )storer   r   tsarchiver   todays          r   
load_stater     s    0 }"""""$##$45B&&22Yrd%3HIGLL))73##%$$$5~!E	 **,;;u$'U3EK EL "ELr   )r:   r>   r=   r>   )r   r   r   r   r   r'   r=   rl   )&r   
__future__r   r   r   r   r   dataclassesr   r   r   enumr   pathlibr   typingr	   core.contractsr
   r   r   r   r   r   rA   rM   rV   r_   re   rl   r   r   r   r   r#   r   r   r   r   r   r   <module>r      sq  ( #  	   0 0    < <      
  
  
F 
 
 
8 $
 
 
8 
P 
P 
P > > >2 
 
 
2 P
 P
 P
n)X	Li( i(`C  (.."	** * 	*
 *r   