o
    A%¸j¬  ã                   @   s`  d Z ddlZddlZddlZddlmZmZ ddlmZ ddlm	Z	 ddl
mZ ddlmZ edƒZeeƒ ¡ jZd	d
„ Zdd„ Zdd„ Zdefdd„Zdefdd„Zdedefdd„Ze	defdd„ƒZd6dedefdd„Zded efd!d"„Zd efd#d$„Zd%efd&d'„Z d(efd)d*„Z!d+efd,d-„Z"d efd.d/„Z#defd0d1„Z$d7d3d4„Z%G d5d2„ d2ƒZ&dS )8a�  
data_store.py

Persistence for this project: ranked candidate lists, open/closed
position state, trade records, and a full per-cycle indicator dump for
every candidate monitor.py evaluates (not just the ones that trade) --
per explicit instruction to log every indicator through a symbol's
whole lifecycle, same reasoning as sip_bot's append_fast_engine_
decision(): the point is being able to load a day's file into pandas
afterward and see exactly what the engine saw, whether or not it ever
became a real trade.

Same atomic-write pattern (write to .tmp, then os.replace) and the same
fcntl-based singleton lock as screener/premarket's data_store.py.
é    N)ÚdateÚdatetime)ÚPath)Úcontextmanager)Ú
get_config)Ú
get_loggerÚ
data_storec                  C   s8   t ƒ d } t| d  t| d  t| d  t| d  dœS )NÚdata_storageÚcandidates_dirÚ
trades_dirÚdecisions_dirÚ	state_dir)Ú
candidatesÚtradesÚ	decisionsÚstate)r   ÚBASE_DIR)Úcfg© r   ú&/var/www/screener/trade1/data_store.pyÚ_dirs    s   




ür   c                  C   s"   t ƒ  ¡ D ]	} | jddd� qd S )NT©ÚparentsÚexist_ok)r   ÚvaluesÚmkdir)Údr   r   r   Úensure_dirs*   s   ÿr   c                   C   s   t  ¡  ¡ S ©N)r   ÚtodayÚ	isoformatr   r   r   r   Ú
_today_str/   ó   r!   Úpathc                 C   sl   | j jddd� |  | jd ¡}t|dƒ�}tj||dtd� W d   ƒ n1 s)w   Y  t 	|| ¡ d S )NTr   z.tmpÚwé   )ÚindentÚdefault)
Úparentr   Úwith_suffixÚsuffixÚopenÚjsonÚdumpÚstrÚosÚreplace)r#   ÚdataÚtmpÚfr   r   r   Ú_atomic_write_json3   s   ÿr4   c              
   C   s�   |   ¡ s|S zt| dƒ�}t |¡W  d   ƒ W S 1 sw   Y  W d S  tjtfyG } zt d| › d|› d�¡ |W  Y d }~S d }~ww )NÚrzFailed to read z: z. Returning default.)Úexistsr+   r,   ÚloadÚJSONDecodeErrorÚOSErrorÚlogÚwarning)r#   r'   r3   Úer   r   r   Ú
_read_json;   s   (ÿ€þr=   Úrecordc                 C   sX   | j jddd� t| dƒ�}| tj|td�d ¡ W d   ƒ d S 1 s%w   Y  d S )NTr   Úa)r'   Ú
)r(   r   r+   Úwriter,   Údumpsr.   )r#   r>   r3   r   r   r   Ú_append_jsonlF   s   "ÿrC   Ú	lock_pathc                 c   sz   � | j jddd� t| dƒ�%}t |tj¡ zd V  W t |tj¡ nt |tj¡ w W d   ƒ d S 1 s6w   Y  d S )NTr   r$   )r(   r   r+   ÚfcntlÚflockÚLOCK_EXÚLOCK_UN)rD   Úlfr   r   r   Ú
_file_lockL   s   €""ûrJ   Ú r   r*   c                 C   sD   t ƒ d tƒ › d|› d� }t|| ƒ t dt| ƒ› d|› �¡ dS )a/  This project's own ranked candidate list -- data/candidates/
    {date}_scanner{suffix}.json. Date-stamped so a day's runs can be reviewed
    and compared later. [2026-09-23] Intraday rescans pass a suffix
    (e.g. "_1030") so they never overwrite the opening scan, which
    simulate.py replays from.r   Ú_scannerz.jsonz[DATA] Wrote z candidates to N)r   r!   r4   r:   ÚinfoÚlen)r   r*   r#   r   r   r   Úwrite_candidatesW   s   
rO   ÚnameÚreturnc                 C   s   t ƒ d |  S )Nr   )r   )rP   r   r   r   Ú_state_pathf   ó   rR   c                   C   s   t tdƒi ƒS )Núpositions.json)r=   rR   r   r   r   r   Úload_positionsj   rS   rU   Ú	positionsc                 C   s@   t tdƒƒ� ttdƒ| ƒ W d   ƒ d S 1 sw   Y  d S )Nzpositions.json.lockrT   )rJ   rR   r4   )rV   r   r   r   Úsave_positionsn   s   "ÿrW   Útradec                 C   s$   t ƒ d tƒ › d� }t|| ƒ dS )zBOne line per closed round trip -- data/trades/{date}_trades.jsonl.r   ú_trades.jsonlN)r   r!   rC   )rX   r#   r   r   r   Úappend_trade_records   s   rZ   r   c                 C   s$   t ƒ d tƒ › d� }t|| ƒ d S )Nr   z_summary.json)r   r!   r4   )r   r#   r   r   r   Úwrite_trades_summaryy   s   r[   c               
   C   s¦   t ƒ d tƒ › d� } |  ¡ sg S g }t| dƒ�1}|D ]%}| ¡ }|s$qz
| t |¡¡ W q tjy@   t	 
d| › �¡ Y qw W d   ƒ |S 1 sLw   Y  |S )Nr   rY   r5   z(Skipping malformed trade record line in )r   r!   r6   r+   ÚstripÚappendr,   Úloadsr8   r:   r;   )r#   r   r3   Úliner   r   r   Úload_today_trades~   s&   ÿú
ÿ	÷	r`   c                 C   s8   t ƒ d tƒ › d� }dt ¡  ¡ i| ¥} t|| ƒ dS )aˆ  One JSON line per rule decision -- data/decisions/{date}.jsonl
    (monitor.py writes one when a symbol's decision state/reason
    changes). Carries whatever metrics the rule module returned, not
    just the classified state, so a day's file can be loaded into pandas and
    show exactly what the engine saw for every symbol at every poll --
    whether or not it ever became a real trade.r   z.jsonlÚ	timestampN)r   r!   r   Úutcnowr    rC   )r>   r#   r   r   r   Úappend_decision_record“   s   rc   ÚSingletonLockc                   C   s   t tdƒƒS )Nzmonitor.pid.lock)rd   rR   r   r   r   r   Úacquire_singleton_lock£   r"   re   c                   @   s*   e Zd Zdefdd„Zdd„ Zdd„ ZdS )	rd   r#   c                 C   s   || _ d | _d S r   )r#   Ú_fh)Úselfr#   r   r   r   Ú__init__¨   s   
zSingletonLock.__init__c                 C   st   | j jjddd� t| j dƒ| _zt | jtjtjB ¡ W n t	y(   t
dƒ‚w | j tt ¡ ƒ¡ | j ¡  | S )NTr   r$   z{Another monitor.py instance already holds the singleton lock. Refusing to start a second instance against the same account.)r#   r(   r   r+   rf   rE   rF   rG   ÚLOCK_NBÚBlockingIOErrorÚRuntimeErrorrA   r.   r/   ÚgetpidÚflush)rg   r   r   r   Ú	__enter__¬   s   ÿÿ
zSingletonLock.__enter__c                 C   s(   | j rt | j tj¡ | j  ¡  d S d S r   )rf   rE   rF   rH   Úclose)rg   Úexc_typeÚexc_valÚexc_tbr   r   r   Ú__exit__º   s   þzSingletonLock.__exit__N)Ú__name__Ú
__module__Ú__qualname__r   rh   rn   rs   r   r   r   r   rd   §   s    )rK   )rQ   rd   )'Ú__doc__rE   r,   r/   r   r   Úpathlibr   Ú
contextlibr   Úconfig_loaderr   Úlogger_setupr   r:   Ú__file__Úresolver(   r   r   r   r!   r4   r=   ÚdictrC   rJ   Úlistr.   rO   rR   rU   rW   rZ   r[   r`   rc   re   rd   r   r   r   r   Ú<module>   s:    


