11"""
2- This module exposes the main 4 endpoints for the Dacos library.
3- No mathematical business logic is allowed here.
2+ DACOS PUBLIC API (THE CONDUCTOR)
3+ Location: src/dacos/api.py
4+ Paradigm: Separation of Concerns, Railway Oriented Programming (ROP), 100% Monadic Boundary.
45"""
56
67from __future__ import annotations
78
89import logging
9- from typing import Any
10+ from typing import Any , cast
1011
1112import numpy as np
1213import polars as pl
1314
1415from dacos .config import StatArbConfig , TSMConfig
15-
16- # Import Engines
17- from dacos .paradigms import compute_pairs_zscore
18-
19- # Import Tactics
20- from dacos .paradigms .stat_arb .tactics import apply_mean_reversion_tactics_strict
21- from dacos .paradigms .tsm .engine import compute_tsm_indicators
22- from dacos .paradigms .tsm .tactics import apply_momentum_tactics_strict
16+ from dacos .paradigms import (
17+ apply_mean_reversion_tactics_strict ,
18+ apply_momentum_tactics_strict ,
19+ compute_pairs_zscore ,
20+ compute_tsm_indicators ,
21+ )
2322from dacos .protocols import DataFrame
2423from dacos .utils import Err , Ok , Result
2524
26- # Asumsi fungsi ingestion tersedia (sesuaikan dengan struktur file Anda)
27- # from dacos.core.data import ingest_silver_data
28-
29- # ============================================================================
30- # EXECUTIVE LOGGER CONFIGURATION
31- # ============================================================================
3225logger = logging .getLogger (__name__ )
3326
34-
3527# ============================================================================
36- # PILAR 2 : THE VECTORIZED RAILWAY (RESEARCH MODE)
28+ # PILAR 1 : THE VECTORIZED RAILWAY (RESEARCH MODE)
3729# ============================================================================
3830
39-
4031def run_stat_arb_research (
41- aligned_data : DataFrame , # Data yang sudah melalui ingest_silver_data & sejajar
32+ aligned_data : DataFrame ,
4233 target_symbol : str ,
4334 anchor_symbol : str ,
4435 hedge_ratio_beta : float ,
4536 config : StatArbConfig | None = None ,
4637) -> Result [DataFrame , ValueError ]:
47- """
48- (Mode 1) Executes a full vectorized backtest pipeline for Statistical Arbitrage.
49- Args:
50- aligned_data: Polars DataFrame containing aligned 'target' and 'anchor' price columns.
51- target_symbol: The coin being traded (Y).
52- anchor_symbol: The hedge coin (X).
53- hedge_ratio_beta: The cointegration multiplier binding X to Y.
54- config: Immutable StatArb configuration. Defaults to StatArbConfig().
55-
56- Returns:
57- Ok(DataFrame) matching STAT_ARB_SIGNAL_SCHEMA, or Err(ValueError).
58- """
5938 config = config or StatArbConfig ()
39+ prefix = f"[STAT-ARB | RESEARCH | { target_symbol } ]"
6040
61- try :
62- logger . info ( f"START [StatArb Research]: Evaluating { target_symbol } vs { anchor_symbol } " )
41+ if not isinstance ( aligned_data , pl . DataFrame ) or aligned_data . is_empty () :
42+ return Err ( ValueError ( f" { prefix } API Rejected: Empty or invalid DataFrame provided." ) )
6343
64- # 1. Stasiun Mesin (Engines)
65- logger .info ("--> Entering Engine Station: Computing Z-Scores..." )
66- engine_res = compute_pairs_zscore (
44+ try :
45+ engine_step = compute_pairs_zscore (
6746 aligned_data = aligned_data ,
6847 target_column = target_symbol ,
6948 anchor_column = anchor_symbol ,
7049 hedge_ratio_beta = hedge_ratio_beta ,
71- z_score_rolling_window = config .z_window ,
50+ z_score_rolling_window = config .z_window
7251 )
73- if engine_res .is_err ():
74- logger .error (f"Engine Failure: { engine_res .unwrap_err ()} " )
75- return engine_res
52+ if engine_step .is_err ():
53+ return Err (engine_step .unwrap_err ()) # Propagate explicitly
7654
77- # 2. Stasiun Taktik & Bea Cukai (Tactics & Schema Enforcement)
78- logger .info ("--> Entering Tactics Station: Translating continuous metrics to discrete signals..." )
79- tactics_res = apply_mean_reversion_tactics_strict (data = engine_res .unwrap (), symbol = target_symbol , config = config )
80- if tactics_res .is_err ():
81- logger .error (f"Tactics Failure: { tactics_res .unwrap_err ()} " )
82- return tactics_res
55+ tactics_step = apply_mean_reversion_tactics_strict (
56+ data = engine_step .unwrap (),
57+ symbol = target_symbol ,
58+ config = config
59+ )
60+ if tactics_step .is_err ():
61+ return Err (tactics_step .unwrap_err ())
8362
84- logger .info (f"SUCCESS [StatArb Research]: Pipeline completed for { target_symbol } ." )
85- return Ok (tactics_res .unwrap ())
63+ # FIX MYPY: Beri tahu MyPy secara eksplisit bahwa hasilnya adalah DataFrame
64+ final_df = cast (pl .DataFrame , tactics_step .unwrap ())
65+ return Ok (final_df )
8666
8767 except Exception as e :
88- logger .error (f"PANIC [StatArb Research]: Unhandled exception breached the pipeline: { e } " )
89- return Err (ValueError (f"StatArb Pipeline Panic: { e } " ))
68+ logger .exception (f"{ prefix } UNHANDLED PANIC. " )
69+ return Err (ValueError (f"StatArb Pipeline Panic: { str ( e ) } " ))
9070
9171
9272def run_tsm_research (
93- silver_data : DataFrame , # Data yang sudah melalui ingest_silver_data
73+ silver_data : DataFrame ,
9474 target_symbol : str ,
9575 config : TSMConfig | None = None ,
9676) -> Result [DataFrame , ValueError ]:
97- """
98- (Mode 1) Executes a full vectorized backtest pipeline for Time Series Momentum (CTA).
99-
100- Args:
101- silver_data: Polars DataFrame containing standard OHLCV columns.
102- target_symbol: The asset being evaluated.
103- config: Immutable TSM configuration. Defaults to TSMConfig().
104-
105- Returns:
106- Ok(DataFrame) matching TSM_SIGNAL_SCHEMA, or Err(ValueError).
107- """
10877 config = config or TSMConfig ()
78+ prefix = f"[TSM | RESEARCH | { target_symbol } ]"
10979
110- try :
111- logger . info ( f"START [TSM Research]: Evaluating { target_symbol } " )
80+ if not isinstance ( silver_data , pl . DataFrame ) or silver_data . is_empty () :
81+ return Err ( ValueError ( f" { prefix } API Rejected: Empty or invalid DataFrame provided." ) )
11282
113- # 1. Stasiun Mesin (Engines)
114- logger .info ("--> Entering Engine Station: Computing Donchian Channels & ATR..." )
115- engine_res = compute_tsm_indicators (
116- data = silver_data , atr_window = config .atr_window , donchian_window = config .donchian_window
83+ try :
84+ engine_step = compute_tsm_indicators (
85+ data = silver_data ,
86+ atr_window = config .atr_window ,
87+ donchian_window = config .donchian_window
11788 )
118- if engine_res .is_err ():
119- logger .error (f"Engine Failure: { engine_res .unwrap_err ()} " )
120- return engine_res
89+ if engine_step .is_err ():
90+ return Err (engine_step .unwrap_err ())
12191
122- # 2. Stasiun Taktik & Bea Cukai (Tactics & Schema Enforcement)
123- logger . info ( "--> Entering Tactics Station: Applying Breakout & Risk Parity sizing..." )
124- tactics_res = apply_momentum_tactics_strict (
125- data = engine_res . unwrap (), target_symbol = target_symbol , config = config
92+ tactics_step = apply_momentum_tactics_strict (
93+ data = engine_step . unwrap (),
94+ target_symbol = target_symbol ,
95+ config = config
12696 )
127- if tactics_res .is_err ():
128- logger .error (f"Tactics Failure: { tactics_res .unwrap_err ()} " )
129- return tactics_res
97+ if tactics_step .is_err ():
98+ return Err (tactics_step .unwrap_err ())
13099
131- logger .info (f"SUCCESS [TSM Research]: Pipeline completed for { target_symbol } ." )
132- return Ok (tactics_res .unwrap ())
100+ # FIX MYPY: Beri tahu MyPy secara eksplisit bahwa hasilnya adalah DataFrame
101+ final_df = cast (pl .DataFrame , tactics_step .unwrap ())
102+ return Ok (final_df )
133103
134104 except Exception as e :
135- logger .error (f"PANIC [TSM Research]: Unhandled exception breached the pipeline: { e } " )
136- return Err (ValueError (f"TSM Pipeline Panic: { e } " ))
105+ logger .exception (f"{ prefix } UNHANDLED PANIC. " )
106+ return Err (ValueError (f"TSM Pipeline Panic: { str ( e ) } " ))
137107
138108
139109# ============================================================================
140110# PILAR 2: THE LIVE TICK PIPELINE (EXECUTION MODE)
141111# ============================================================================
142112
143-
144113def evaluate_stat_arb_live (
145114 live_buffer : DataFrame | dict [str , Any ],
146115 target_symbol : str ,
147116 anchor_symbol : str ,
148117 hedge_ratio_beta : float ,
149118 config : StatArbConfig | None = None ,
150119) -> Result [dict [str , Any ], ValueError ]:
151- """
152- (Mode 2) High-speed evaluation of a single live tick/buffer for Statistical Arbitrage.
153- Bypasses Ingestion and Cointegration Testing.
154-
155- Args:
156- live_buffer: Small DataFrame or Dict containing the recent lookback window.
157- target_symbol: The coin being traded (Y).
158- anchor_symbol: The hedge coin (X).
159- hedge_ratio_beta: The pre-calculated beta from the research phase.
160- config: Immutable StatArb configuration.
161-
162- Returns:
163- Ok(Dict) containing exactly one actionable signal matching STAT_ARB_SIGNAL_SCHEMA.
164- """
165-
166120 config = config or StatArbConfig ()
167121
168122 try :
169- # SURGERY: Mesin StatArb Polars mewajibkan DataFrame. Casting dict (20 rows) membutuhkan < 1ms.
170123 df_buffer = pl .DataFrame (live_buffer ) if isinstance (live_buffer , dict ) else live_buffer
171124
172125 engine_step = compute_pairs_zscore (
173126 aligned_data = df_buffer ,
174127 target_column = target_symbol ,
175128 anchor_column = anchor_symbol ,
176129 hedge_ratio_beta = hedge_ratio_beta ,
177- z_score_rolling_window = config .z_window ,
130+ z_score_rolling_window = config .z_window
178131 )
179132 if engine_step .is_err ():
180- return engine_step
133+ return Err ( engine_step . unwrap_err ())
181134
182- # Ambil baris terujung (Latest Tick) dan suapkan ke Tactics Mode 2 (Dict)
183135 latest_tick = engine_step .unwrap ().row (- 1 , named = True )
184- return apply_mean_reversion_tactics_strict (data = latest_tick , symbol = target_symbol , config = config )
136+ tactics_res = apply_mean_reversion_tactics_strict (
137+ data = latest_tick ,
138+ symbol = target_symbol ,
139+ config = config
140+ )
141+ if tactics_res .is_err ():
142+ return Err (tactics_res .unwrap_err ())
143+
144+ # FIX MYPY: Casting eksplisit ke Dict
145+ final_dict = cast (dict [str , Any ], tactics_res .unwrap ())
146+ return Ok (final_dict )
185147
186148 except Exception as e :
187- logger .error (f"[STAT-ARB | LIVE | { target_symbol } ] Panic: { e } " )
188149 return Err (ValueError (f"StatArb Live Panic: { str (e )} " ))
189150
190151
@@ -193,46 +154,52 @@ def evaluate_tsm_live(
193154 target_symbol : str ,
194155 config : TSMConfig | None = None ,
195156) -> Result [dict [str , Any ], ValueError ]:
196- """(Mode 2) High-speed evaluation of a single live tick/buffer for Time Series Momentum."""
197157 config = config or TSMConfig ()
198158
199159 try :
200160 engine_step = compute_tsm_indicators (
201- data = live_buffer , atr_window = config .atr_window , donchian_window = config .donchian_window
161+ data = live_buffer ,
162+ atr_window = config .atr_window ,
163+ donchian_window = config .donchian_window
202164 )
203165 if engine_step .is_err ():
204- return engine_step
166+ return Err ( engine_step . unwrap_err ())
205167
206168 engine_data = engine_step .unwrap ()
207169
208- # SURGERY: Rekonstruksi Dict jika data dari Numba (yang tidak membawa timestamp)
209- if isinstance (live_buffer , dict ):
210- # Ambil elemen terakhir dari array timestamp/closets
211- ts = (
212- live_buffer ["timestamp" ][- 1 ]
213- if isinstance (live_buffer ["timestamp" ], list | np .ndarray )
214- else live_buffer ["timestamp" ]
215- )
216- cl = (
217- live_buffer ["close" ][- 1 ]
218- if isinstance (live_buffer ["close" ], list | np .ndarray )
219- else live_buffer ["close" ]
220- )
170+ # FIX MYPY: Evaluasi `engine_data` secara eksplisit agar MyPy paham itu Dict atau DataFrame
171+ if isinstance (engine_data , dict ):
172+ # Casting aman karena jika masuk sini, live_buffer pasti dict yang punya list/ndarray
173+ ts_list = cast (Any , live_buffer )["timestamp" ]
174+ cl_list = cast (Any , live_buffer )["close" ]
175+
176+ ts = ts_list [- 1 ] if isinstance (ts_list , list | np .ndarray ) else ts_list
177+ cl = cl_list [- 1 ] if isinstance (cl_list , list | np .ndarray ) else cl_list
221178
222179 engine_data ["timestamp" ] = ts
223180 engine_data ["close" ] = cl
224181 latest_tick = engine_data
225-
226- else :
182+ elif isinstance (engine_data , pl .DataFrame ):
227183 latest_tick = engine_data .row (- 1 , named = True )
184+ else :
185+ return Err (ValueError ("Engine returned invalid data type." ))
228186
229- return apply_momentum_tactics_strict (data = latest_tick , target_symbol = target_symbol , config = config )
187+ tactics_res = apply_momentum_tactics_strict (
188+ data = latest_tick ,
189+ target_symbol = target_symbol ,
190+ config = config
191+ )
192+
193+ if tactics_res .is_err ():
194+ return Err (tactics_res .unwrap_err ())
195+
196+ # FIX MYPY: Casting eksplisit ke Dict
197+ final_dict = cast (dict [str , Any ], tactics_res .unwrap ())
198+ return Ok (final_dict )
230199
231200 except Exception as e :
232- logger .error (f"[TSM | LIVE | { target_symbol } ] Panic: { e } " )
233201 return Err (ValueError (f"TSM Live Panic: { str (e )} " ))
234202
235-
236203__all__ = [
237204 "run_stat_arb_research" ,
238205 "run_tsm_research" ,
0 commit comments