2026-02-22 09:42:41 -05:00
# kennis_streamlit.py
2026-02-20 18:06:00 -05:00
import streamlit as st
import numpy as np
import pandas as pd
2026-02-22 09:42:41 -05:00
from io import BytesIO
import plotly.graph_objects as go
2026-02-20 18:06:00 -05:00
2026-02-22 10:54:37 -05:00
# ------------------ Configuración de la página ------------------
2026-02-22 10:42:16 -05:00
st . set_page_config ( page_title = "Kennis FX Markets" , layout = "wide" , initial_sidebar_state = "expanded" )
2026-02-22 09:42:41 -05:00
st . markdown (
"""
<style>
2026-02-22 10:54:37 -05:00
.stApp { background-color: #f4f6f9; color: #1a1f2b; font-family: 'Segoe UI', Roboto, Arial; }
.header { color: #0f2742; font-weight:700; font-size:22px; }
.subtitle { color: #4a6578; margin-top:-6px; }
.card { background:#ffffff; padding:14px; border-radius:10px; box-shadow: 0 2px 8px rgba(20,30,40,0.06); }
.muted { color:#5b6b75; font-size:13px; }
.decision { font-weight:700; font-size:18px; color:#0f2742; }
.explain { color:#22303a; font-size:14px; line-height:1.45; }
.small { font-size:12px; color:#6f8190; }
hr { border:none; border-top: 1px solid #e6eef8; }
2026-02-22 11:45:27 -05:00
.warning { color:#8a6d3b; }
2026-02-22 09:42:41 -05:00
</style>
""" ,
unsafe_allow_html = True ,
)
2026-02-20 18:06:00 -05:00
2026-02-22 10:54:37 -05:00
# ------------------ Header / Hero ------------------
2026-02-22 09:42:41 -05:00
col1 , col2 = st . columns ([ 4 , 1 ])
with col1 :
2026-02-22 10:42:16 -05:00
st . markdown ( "<div class='header'>Kennis FX Markets</div>" , unsafe_allow_html = True )
st . markdown ( "<div class='subtitle'>Institutional Bayesian Intelligence Engine</div>" , unsafe_allow_html = True )
2026-02-22 10:54:37 -05:00
st . markdown ( "<div class='muted'>Detection and strategic allocation guidance. (Edición táctica — swing / corto plazo)</div>" , unsafe_allow_html = True )
2026-02-22 09:42:41 -05:00
with col2 :
2026-02-22 10:42:16 -05:00
st . markdown ( "<div class='card'><div class='small'>Kennis FX — Tactical edition</div></div>" , unsafe_allow_html = True )
2026-02-22 09:42:41 -05:00
st . markdown ( "---" )
2026-02-22 10:54:37 -05:00
# ------------------ Sidebar: Inputs & opciones ------------------
st . sidebar . header ( "Entradas (táctica / swing)" )
2026-02-22 11:24:02 -05:00
uploaded = st . sidebar . file_uploader (
"Sube CSV histórico (opcional). Columnas requeridas: date,cot_long,cot_short,fed_prob,retail_pct,target_up" ,
type = [ "csv" ]
)
2026-02-22 10:54:37 -05:00
st . sidebar . markdown ( "O ingresa la fila live abajo:" )
2026-02-22 09:42:41 -05:00
2026-02-22 11:10:54 -05:00
par = st . sidebar . selectbox (
"Par de divisas" ,
[
# Majors
"USD/JPY" , "EUR/USD" , "GBP/USD" , "AUD/USD" , "USD/CHF" , "USD/CAD" , "NZD/USD" ,
"EUR/GBP" , "EUR/JPY" , "GBP/JPY" ,
# Principales exóticas / emergentes y crosses relevantes
"USD/SGD" , "USD/MXN" , "USD/BRL" , "USD/ZAR" , "USD/TRY" , "USD/HKD" , "EUR/TRY" , "GBP/TRY"
],
index = 0
)
2026-02-22 10:42:16 -05:00
cot_long = st . sidebar . number_input ( "COT No-Commercial — Long" , value = 13662 , step = 1 )
cot_short = st . sidebar . number_input ( "COT No-Commercial — Short" , value = 13546 , step = 1 )
2026-02-22 10:54:37 -05:00
fed_prob = st . sidebar . number_input ( "FedWatch Prob (%) para bin actual" , min_value = 0.0 , max_value = 100.0 , value = 96.0 , step = 0.1 )
2026-02-22 09:42:41 -05:00
retail_pct = st . sidebar . number_input ( "Retail % Buy (0-100)" , min_value = 0.0 , max_value = 100.0 , value = 47.0 , step = 0.1 )
st . sidebar . markdown ( "---" )
2026-02-22 10:54:37 -05:00
st . sidebar . header ( "Modelo y normalización" )
s_cot = st . sidebar . number_input ( "S_cot (escala para COT)" , value = 50000 , step = 1000 )
prior_var = st . sidebar . number_input ( "Varianza prior gaussiano" , value = 0.5 , step = 0.1 )
retrain = st . sidebar . button ( "Reentrenar (si subes CSV)" )
2026-02-22 09:42:41 -05:00
2026-02-22 10:54:37 -05:00
# ------------------ Utilidades matemáticas ------------------
2026-02-20 18:06:00 -05:00
def sigmoid ( x ):
2026-02-22 09:42:41 -05:00
return 1.0 / ( 1.0 + np . exp ( - x ))
2026-02-20 18:06:00 -05:00
2026-02-22 09:42:41 -05:00
def fit_bayesian_logit ( X , y , prior_var = 0.5 , maxiter = 200 , tol = 1e-8 ):
2026-02-20 18:06:00 -05:00
n , p = X . shape
beta = np . zeros ( p )
prior_prec = np . eye ( p ) / prior_var
2026-02-22 11:24:02 -05:00
H = np . eye ( p ) * 1e-6
2026-02-22 09:42:41 -05:00
for i in range ( maxiter ):
2026-02-20 18:06:00 -05:00
eta = X . dot ( beta )
p_vec = sigmoid ( eta )
2026-02-22 09:42:41 -05:00
W = p_vec * ( 1.0 - p_vec )
2026-02-22 10:42:16 -05:00
W = np . clip ( W , 1e-8 , None )
2026-02-20 18:06:00 -05:00
H = X . T . dot ( W [:, None ] * X ) + prior_prec
grad = X . T . dot ( y - p_vec ) - prior_prec . dot ( beta )
try :
delta = np . linalg . solve ( H , grad )
2026-02-22 09:42:41 -05:00
except np . linalg . LinAlgError :
H += np . eye ( p ) * 1e-8
2026-02-20 18:06:00 -05:00
delta = np . linalg . solve ( H , grad )
2026-02-22 09:42:41 -05:00
beta = beta + delta
if np . linalg . norm ( delta ) < tol :
2026-02-20 18:06:00 -05:00
break
2026-02-22 11:24:02 -05:00
try :
cov = np . linalg . inv ( H )
except np . linalg . LinAlgError :
cov = np . linalg . pinv ( H )
2026-02-20 18:06:00 -05:00
return beta , cov
2026-02-22 10:54:37 -05:00
def to_excel_bytes ( df ):
2026-02-22 10:42:16 -05:00
out = BytesIO ()
with pd . ExcelWriter ( out , engine = "openpyxl" ) as writer :
2026-02-22 09:42:41 -05:00
df . to_excel ( writer , index = False , sheet_name = "signal" )
2026-02-22 10:42:16 -05:00
return out . getvalue ()
2026-02-20 18:06:00 -05:00
2026-02-22 10:54:37 -05:00
# ------------------ Ingesta de datos ------------------
2026-02-22 11:45:27 -05:00
using_demo = False
2026-02-22 09:42:41 -05:00
if uploaded is not None :
2026-02-22 10:54:37 -05:00
try :
df = pd . read_csv ( uploaded )
st . info ( f "CSV cargado: { uploaded . name } — { len ( df ) } filas" )
except Exception as e :
st . error ( "Error leyendo CSV: " + str ( e ))
df = pd . DataFrame ()
2026-02-22 11:45:27 -05:00
using_demo = True
2026-02-22 09:42:41 -05:00
else :
2026-02-22 11:45:27 -05:00
using_demo = True
2026-02-22 09:42:41 -05:00
np . random . seed ( 42 )
2026-02-22 10:42:16 -05:00
N = 420
2026-02-22 09:42:41 -05:00
cot_long_demo = np . random . normal ( 12000 , 3000 , size = N ) . astype ( int )
cot_short_demo = np . random . normal ( 11000 , 3000 , size = N ) . astype ( int )
2026-02-22 10:42:16 -05:00
fed_demo = np . clip ( np . random . normal ( 60 , 12 , size = N ), 0 , 100 )
retail_demo = np . clip ( np . random . normal ( 50 , 12 , size = N ), 0 , 100 )
2026-02-22 09:42:41 -05:00
net_demo = cot_long_demo - cot_short_demo
2026-02-22 11:24:02 -05:00
z_net_demo = ( net_demo - net_demo . mean ()) / ( net_demo . std () if net_demo . std () > 0 else 1 )
true_score = 0.8 * z_net_demo + 1.5 * (( fed_demo - 50 ) / 50 ) - 0.5 * (( retail_demo - 50 ) / 50 )
2026-02-22 09:42:41 -05:00
prob_demo = sigmoid ( true_score )
target_demo = ( np . random . rand ( N ) < prob_demo ) . astype ( int )
df = pd . DataFrame ({
2026-02-22 10:54:37 -05:00
"date" : pd . date_range ( end = pd . Timestamp . today (), periods = N ),
2026-02-22 09:42:41 -05:00
"cot_long" : cot_long_demo , "cot_short" : cot_short_demo ,
"fed_prob" : fed_demo , "retail_pct" : retail_demo ,
"target_up" : target_demo
})
2026-02-22 10:54:37 -05:00
st . info ( "Usando datos demo. Suba CSV histórico para calibración con datos reales." )
2026-02-20 18:06:00 -05:00
2026-02-22 10:54:37 -05:00
# ------------------ Feature engineering ------------------
2026-02-22 10:42:16 -05:00
def build_features ( df_in , s_cot_local = 50000 ):
df = df_in . copy ()
2026-02-22 09:42:41 -05:00
df [ "net" ] = df [ "cot_long" ] - df [ "cot_short" ]
df [ "net_scaled" ] = df [ "net" ] / float ( s_cot_local )
df [ "z_net" ] = ( df [ "net" ] - df [ "net" ] . rolling ( 252 , min_periods = 1 ) . mean ()) / df [ "net" ] . rolling ( 252 , min_periods = 1 ) . std () . replace ( 0 , 1 )
df [ "delta_1w" ] = df [ "net" ] . diff ( 5 ) . fillna ( 0 )
df [ "delta_4w" ] = df [ "net" ] . diff ( 20 ) . fillna ( 0 )
df [ "z_delta_1w" ] = ( df [ "delta_1w" ] - df [ "delta_1w" ] . rolling ( 252 , min_periods = 1 ) . mean ()) / df [ "delta_1w" ] . rolling ( 252 , min_periods = 1 ) . std () . replace ( 0 , 1 )
df [ "z_delta_4w" ] = ( df [ "delta_4w" ] - df [ "delta_4w" ] . rolling ( 252 , min_periods = 1 ) . mean ()) / df [ "delta_4w" ] . rolling ( 252 , min_periods = 1 ) . std () . replace ( 0 , 1 )
df [ "signal_fed" ] = ( df [ "fed_prob" ] - 50.0 ) / 50.0
df [ "signal_retail" ] = ( df [ "retail_pct" ] - 50.0 ) / 50.0
return df
2026-02-20 18:06:00 -05:00
2026-02-22 09:42:41 -05:00
df_feat = build_features ( df , s_cot_local = s_cot )
2026-02-20 18:06:00 -05:00
2026-02-22 10:54:37 -05:00
# Features used in el modelo táctico
2026-02-22 09:42:41 -05:00
feature_cols = [ "z_net" , "z_delta_1w" , "z_delta_4w" , "signal_fed" , "signal_retail" ]
2026-02-22 10:42:16 -05:00
2026-02-22 09:42:41 -05:00
if "target_up" in df_feat . columns :
train_df = df_feat . copy ()
X_train = train_df [ feature_cols ] . fillna ( 0 ) . values
y_train = train_df [ "target_up" ] . astype ( int ) . values
else :
2026-02-22 11:45:27 -05:00
# Should not happen because demo includes target_up, but keep safe fallback
2026-02-22 09:42:41 -05:00
train_df = df_feat . copy ()
X_train = train_df [ feature_cols ] . fillna ( 0 ) . values
2026-02-22 11:45:27 -05:00
y_train = df_feat . get ( "target_up" , pd . Series ( np . zeros ( len ( train_df )))) . astype ( int ) . values
2026-02-20 18:06:00 -05:00
2026-02-22 10:42:16 -05:00
if retrain and uploaded is None :
2026-02-22 10:54:37 -05:00
st . warning ( "Para reentrenar debe subir un CSV histórico con etiqueta 'target_up'." )
2026-02-20 18:06:00 -05:00
2026-02-22 10:54:37 -05:00
with st . spinner ( "Calibrando modelo bayesiano táctico (MAP + Laplace)..." ):
try :
beta_map , cov_post = fit_bayesian_logit ( X_train , y_train , prior_var = prior_var )
except Exception as e :
st . error ( "Fallo en calibración del modelo: " + str ( e ))
beta_map = np . zeros ( len ( feature_cols ))
cov_post = np . eye ( len ( feature_cols )) * 1.0
# ------------------ Fila en vivo y predicción ------------------
2026-02-22 10:42:16 -05:00
live = { "cot_long" : float ( cot_long ), "cot_short" : float ( cot_short ), "fed_prob" : float ( fed_prob ), "retail_pct" : float ( retail_pct )}
aug = pd . concat ([ df . head ( 1 ) . copy (), pd . DataFrame ([ live ])], ignore_index = True )
live_feat = build_features ( aug , s_cot_local = s_cot ) . iloc [ - 1 ]
2026-02-22 09:42:41 -05:00
x_live = live_feat [ feature_cols ] . fillna ( 0 ) . values
2026-02-22 11:24:02 -05:00
net = float ( live_feat [ "net" ])
2026-02-20 18:06:00 -05:00
2026-02-22 11:45:27 -05:00
# Posterior sampling + predicción (defensiva) + calibración contra sesgo histórico
sample_probs_adj = None
2026-02-22 13:03:37 -05:00
calibration_applied = False
2026-02-22 09:42:41 -05:00
try :
2026-02-22 11:24:02 -05:00
cov_post = np . array ( cov_post )
cov_post = 0.5 * ( cov_post + cov_post . T )
jitter = 1e-8 * np . eye ( cov_post . shape [ 0 ])
samples = np . random . multivariate_normal ( beta_map , cov_post + jitter , size = 4000 )
2026-02-22 09:42:41 -05:00
sample_probs = sigmoid ( samples . dot ( x_live ))
2026-02-22 11:45:27 -05:00
# Calibración: centramos la predicción para neutralizar el sesgo medio en entrenamiento
try :
train_eta = X_train . dot ( beta_map )
train_mean_p = float ( sigmoid ( train_eta ) . mean ())
except Exception :
train_mean_p = float ( sigmoid ( X_train . dot ( beta_map )) . mean ()) if X_train . size else 0.5
bias_offset = train_mean_p - 0.5 # positivo => modelo históricamente overpredicts up
# ajustar sample probs y recortar entre 0 y 1
sample_probs_adj = np . clip ( sample_probs - bias_offset , 0.0 , 1.0 )
p_mean = float ( sample_probs_adj . mean ())
p_low = float ( np . percentile ( sample_probs_adj , 2.5 ))
p_high = float ( np . percentile ( sample_probs_adj , 97.5 ))
calibration_applied = abs ( bias_offset ) > 1e-6
2026-02-22 10:42:16 -05:00
except Exception :
eta = float ( x_live . dot ( beta_map ))
2026-02-22 09:42:41 -05:00
p_mean = float ( sigmoid ( eta ))
p_low , p_high = p_mean , p_mean
2026-02-22 11:45:27 -05:00
sample_probs_adj = np . array ([ p_mean ])
calibration_applied = False
2026-02-20 18:06:00 -05:00
2026-02-22 11:45:27 -05:00
# Índice de convicción (usando distribución calibrada si existe)
dispersion = max ( 1e-9 , float ( np . std ( sample_probs_adj )))
2026-02-22 10:42:16 -05:00
conviction = min ( 100.0 , max ( 0.0 , 100.0 * ( abs ( p_mean - 0.5 ) * 2.0 ) * ( 1.0 / ( 1.0 + 5.0 * dispersion ))))
conviction = round ( conviction , 1 )
2026-02-22 11:45:27 -05:00
# Decisión táctica profesional (probabilidad + convicción)
if conviction < 35 :
decision = "ESPERAR (NO OPERAR)"
decision_flag = "wait"
elif p_mean >= 0.60 :
2026-02-22 09:42:41 -05:00
decision = "COMPRAR (LONG)"
2026-02-22 10:42:16 -05:00
decision_flag = "buy"
2026-02-22 09:42:41 -05:00
elif p_mean <= 0.40 :
decision = "VENDER (SHORT)"
2026-02-22 10:42:16 -05:00
decision_flag = "sell"
2026-02-22 09:42:41 -05:00
else :
decision = "ESPERAR (NO OPERAR)"
2026-02-22 10:42:16 -05:00
decision_flag = "wait"
2026-02-20 18:06:00 -05:00
2026-02-22 10:54:37 -05:00
# Descomposición de contribuciones
2026-02-22 10:42:16 -05:00
contrib_raw = np . array ( beta_map ) * np . array ( x_live )
if np . sum ( np . abs ( contrib_raw )) > 0 :
contrib_pct = 100.0 * contrib_raw / np . sum ( np . abs ( contrib_raw ))
else :
contrib_pct = np . zeros_like ( contrib_raw )
2026-02-22 13:03:37 -05:00
# ------------------ Módulo Intradía (Híbrido Profesional) ------------------
st . markdown ( "---" )
st . markdown ( "## Intraday Execution Engine" )
activate_intraday = st . checkbox ( "Activar módulo intradía (requiere bias macro activo)" )
if activate_intraday :
# Solo permitir intradía si bias macro activo y convicción suficiente
if decision_flag == "wait" or conviction < 35 :
st . warning ( "Bias macro insuficiente o convicción baja. Intradía desactivado hasta nueva señal." )
intraday_signal = "NO TRADE"
intraday_info = {}
else :
# Parámetros de la demo (reemplazar por feed real en producción)
st . markdown ( "### Parámetros intradía (demo)" )
# Simulación de datos intradía (1m-like series) — sustituir por feed real
np . random . seed ( 7 )
M = 300
intraday_returns = np . random . normal ( 0 , 0.0008 , M ) # retornos pequeños
intraday_prices = 100.0 + np . cumsum ( intraday_returns )
intraday_volume = np . random . randint ( 80 , 200 , M )
df_intraday = pd . DataFrame ({
"price" : intraday_prices ,
"volume" : intraday_volume
})
# VWAP
df_intraday [ "cum_vol" ] = df_intraday [ "volume" ] . cumsum ()
df_intraday [ "cum_pv" ] = ( df_intraday [ "price" ] * df_intraday [ "volume" ]) . cumsum ()
# evitar división por cero
df_intraday [ "vwap" ] = df_intraday [ "cum_pv" ] / df_intraday [ "cum_vol" ] . replace ( 0 , np . nan )
df_intraday [ "vwap" ] . fillna ( method = "ffill" , inplace = True )
current_price = float ( df_intraday [ "price" ] . iloc [ - 1 ])
current_vwap = float ( df_intraday [ "vwap" ] . iloc [ - 1 ])
# ATR proxy (rolling mean absolute diff)
df_intraday [ "returns_abs" ] = df_intraday [ "price" ] . diff () . abs ()
atr = float ( df_intraday [ "returns_abs" ] . rolling ( 14 , min_periods = 1 ) . mean () . iloc [ - 1 ])
# Session high / low
session_high = float ( df_intraday [ "price" ] . max ())
session_low = float ( df_intraday [ "price" ] . min ())
# Tick imbalance proxy
up_ticks = int (( df_intraday [ "price" ] . diff () > 0 ) . sum ())
down_ticks = int (( df_intraday [ "price" ] . diff () < 0 ) . sum ())
denom = max ( 1 , up_ticks + down_ticks )
imbalance = float (( up_ticks - down_ticks ) / denom )
# Confirmación estructural simple
intraday_signal = "NO TRADE"
if decision_flag == "buy" :
# criterio: precio por encima de VWAP y imbalance positivo
if ( current_price > current_vwap ) and ( imbalance > 0 ):
intraday_signal = "LONG CONFIRMADO"
elif decision_flag == "sell" :
if ( current_price < current_vwap ) and ( imbalance < 0 ):
intraday_signal = "SHORT CONFIRMADO"
intraday_info = {
"current_price" : current_price ,
"current_vwap" : current_vwap ,
"atr" : atr ,
"session_high" : session_high ,
"session_low" : session_low ,
"imbalance" : imbalance ,
"signal" : intraday_signal
}
# Mostrar resultados intradía
st . markdown ( "### Estado intradía (demo)" )
colA , colB , colC = st . columns ( 3 )
with colA :
st . metric ( "Precio actual" , f " { current_price : .5f } " )
st . metric ( "VWAP" , f " { current_vwap : .5f } " )
with colB :
st . metric ( "ATR (proxy)" , f " { atr : .6f } " )
st . metric ( "Imbalance" , f " { imbalance : .3f } " )
with colC :
st . metric ( "Session High" , f " { session_high : .5f } " )
st . metric ( "Session Low" , f " { session_low : .5f } " )
if intraday_signal == "LONG CONFIRMADO" :
st . success ( "Señal Intradía: LONG confirmado con VWAP + Imbalance" )
elif intraday_signal == "SHORT CONFIRMADO" :
st . error ( "Señal Intradía: SHORT confirmado con VWAP + Imbalance" )
else :
st . info ( "No hay confirmación intradía aún. Esperar estructura o mayor desequilibrio." )
# Plan de ejecución sugerido (demo)
st . markdown ( "### Plan de Ejecución (demo)" )
if intraday_signal in [ "LONG CONFIRMADO" , "SHORT CONFIRMADO" ]:
st . markdown ( f """
- Entrada en dirección del bias macro ( { decision } ).
- Stop técnico ≈ 1.2 × ATR (≈ { 1.2 * atr : .6f } ).
- Target inicial ≥ 1.5 × riesgo.
- Tamaño sugerido: 0.5– 1% del capital.
""" )
else :
st . markdown ( "Esperar ruptura estructural o mayor desequilibrio de flujo." )
else :
intraday_signal = "NO TRADE"
intraday_info = {}
2026-02-22 11:10:54 -05:00
# ------------------ Narrativa simplificada + técnica (en español) ------------------
2026-02-22 11:45:27 -05:00
def narrative_tactical_simplificada ( p_mean , p_low , p_high , conviction , decision_flag , x_live , contrib_pct , feature_cols , par , calibration_applied , using_demo ):
2026-02-22 11:10:54 -05:00
"""
Versión legible para usuario promedio + explicación técnica desplegable.
Devuelve (texto_simple, texto_tecnico).
"""
2026-02-22 11:24:02 -05:00
prob_text = f "P( { par } ↑) ≈ { p_mean * 100 : .1f } % (IC95%: { p_low * 100 : .1f } %– { p_high * 100 : .1f } %). Convicción: { conviction } /100."
if decision_flag == "buy" :
simple = (
"Recomendación: **Comprar / Abrir posición larga (táctica)**. \n\n "
"Por qué (simple): la probabilidad de subida es alta y hay soporte estructural. "
"Se sugiere entrar de forma gradual y controlar el riesgo con stops."
)
elif decision_flag == "sell" :
simple = (
"Recomendación: **Vender / Abrir posición corta (táctica)**. \n\n "
"Por qué (simple): la probabilidad favorece la baja y la convicción es suficiente para una operación táctica. "
"Usa stops ajustados y tamaño reducido."
)
else :
simple = (
"Recomendación: **No operar (esperar)**. \n\n "
2026-02-22 11:45:27 -05:00
"Por qué (simple): la probabilidad está cerca de equilibrio o la convicción es baja; mejor esperar confirmación."
2026-02-22 11:24:02 -05:00
)
2026-02-22 11:45:27 -05:00
# Texto técnico
2026-02-22 13:03:37 -05:00
fed_s = x_live [ 3 ] if len ( x_live ) > 3 else 0.0
cot_s = x_live [ 0 ] if len ( x_live ) > 0 else 0.0
retail_s = x_live [ 4 ] if len ( x_live ) > 4 else 0.0
2026-02-22 11:24:02 -05:00
tech_lines = []
tech_lines . append ( f "Resumen técnico — activo: { par } " )
tech_lines . append ( f "- Probabilidad bayesiana P( { par } ↑) = { p_mean * 100 : .1f } % (IC95%: { p_low * 100 : .1f } %– { p_high * 100 : .1f } %). Convicción: { conviction } /100." )
2026-02-22 11:45:27 -05:00
if calibration_applied :
tech_lines . append ( "- Nota técnica: la predicción ha sido calibrada contra el sesgo medio del histórico para mejorar neutralidad." )
if using_demo :
tech_lines . append ( "- Advertencia: se están usando datos demo para calibración. Suba su CSV histórico para calibración real y backtests." )
2026-02-22 11:24:02 -05:00
# Macro
if fed_s > 0.4 :
tech_lines . append ( "- Macro: FedWatch con sesgo hawkish → favorece USD fuerte en el corto plazo." )
elif fed_s < - 0.4 :
tech_lines . append ( "- Macro: FedWatch dovish → presión a la baja para USD." )
else :
tech_lines . append ( "- Macro: FedWatch neutral/moderado → macro no es el driver dominante ahora." )
# COT
if abs ( cot_s ) < 0.15 :
tech_lines . append ( "- Posicionamiento (COT): cercano a neutral — riesgo de sobreacumulación limitado." )
elif cot_s >= 0.15 :
tech_lines . append ( "- Posicionamiento (COT): sesgo long entre no-commercials — soporte estructural pero atención a toma de ganancias." )
else :
tech_lines . append ( "- Posicionamiento (COT): sesgo short — riesgo de short-squeeze si macro gira hawkish." )
# Retail
if retail_s > 0.15 :
tech_lines . append ( "- Retail: minoristas inclinados a comprar — posible señal contraria a corto plazo." )
elif retail_s < - 0.15 :
tech_lines . append ( "- Retail: minoristas inclinados a vender — puede reforzar momentum bajista si lo institucional confirma." )
else :
tech_lines . append ( "- Retail: posicionamiento minorista balanceado — no prevalece contrarian ahora." )
2026-02-22 11:45:27 -05:00
# Contribuciones
2026-02-22 11:24:02 -05:00
tech_lines . append ( "- Descomposición de contribuciones (valores y % f irmadas):" )
for name , pct , val in zip ( feature_cols , contrib_pct , x_live ):
sign = "+" if pct >= 0 else "-"
tech_lines . append ( f " • { name } : { sign }{ abs ( pct ) : .1f } % (valor { val : .3f } )" )
2026-02-22 11:45:27 -05:00
# Razonamiento
2026-02-22 11:24:02 -05:00
if decision_flag == "buy" :
2026-02-22 11:45:27 -05:00
tech_lines . append ( "- Recomendación táctica: acumulación gradual LONG. Ejecución: escala en 3 tramos; tamaño inicial 0.5– 1% notional; stop 1.0– 1.5× ATR; R:R ≥ 1:1.5." )
2026-02-22 11:24:02 -05:00
elif decision_flag == "sell" :
tech_lines . append ( "- Recomendación táctica: considerar SHORT táctico o reducir largos. Ejecución: tamaño reducido; stops ajustados; vigilar noticias." )
else :
tech_lines . append ( "- Recomendación táctica: no operar. Esperar confirmación de flujos o ruptura técnica." )
tech_lines . append ( "- Nota: COT es semanal (retardo); combine con order-flow intradía y skew de opciones para ejecución." )
texto_tecnico = " \n " . join ( tech_lines )
texto_simple = prob_text + " \n\n " + simple
return texto_simple , texto_tecnico
# Obtener textos simple y técnico
texto_simple , texto_tecnico = narrative_tactical_simplificada (
2026-02-22 11:45:27 -05:00
p_mean , p_low , p_high , conviction , decision_flag , x_live , contrib_pct , feature_cols , par , calibration_applied , using_demo
2026-02-22 11:24:02 -05:00
)
# ------------------ Interfaz: resultado y narrativa ------------------
left , right = st . columns ([ 2 , 3 ])
with left :
st . markdown ( "<div class='card'>" , unsafe_allow_html = True )
st . markdown ( f "<div class='decision'> { decision } </div>" , unsafe_allow_html = True )
st . markdown (
f "<div class='muted'>Probabilidad (media): <strong> { p_mean * 100 : .1f } %</strong> 95% IC: <strong> { p_low * 100 : .1f } %– { p_high * 100 : .1f } %</strong></div>" ,
unsafe_allow_html = True
)
st . markdown ( f "<div class='muted'>Convicción: <strong> { conviction } /100</strong></div>" , unsafe_allow_html = True )
2026-02-22 11:45:27 -05:00
if calibration_applied :
st . markdown ( "<div class='small warning'>Se aplicó calibración contra el sesgo histórico del modelo para favorecer neutralidad.</div>" , unsafe_allow_html = True )
if using_demo :
st . markdown ( "<div class='small warning'>ATENCIÓN: usando datos demo. Suba CSV histórico para calibración real.</div>" , unsafe_allow_html = True )
2026-02-22 11:24:02 -05:00
st . markdown ( "<hr/>" , unsafe_allow_html = True )
st . markdown ( "### Plan táctico (resumen)" , unsafe_allow_html = True )
if decision_flag == "buy" :
st . markdown ( "- Escala en 3 tramos. Tramo inicial 0.5– 1% notional." , unsafe_allow_html = True )
st . markdown ( "- Stop: 1.0– 1.5× ATR o por debajo de soporte técnico." , unsafe_allow_html = True )
st . markdown ( "- Target: R:R ≥ 1:1.5." , unsafe_allow_html = True )
elif decision_flag == "sell" :
st . markdown ( "- Reducir exposiciones largas o iniciar cortos pequeños (0.5– 1%)." , unsafe_allow_html = True )
st . markdown ( "- Stops estrictos; vigila comunicados macro." , unsafe_allow_html = True )
else :
st . markdown ( "- No operar. Esperar señal clara o confirmación de flujos." , unsafe_allow_html = True )
st . markdown ( "</div>" , unsafe_allow_html = True )
with right :
fig = go . Figure ( go . Indicator (
mode = "gauge+number" ,
value = p_mean * 100 ,
number = { 'suffix' : '%' },
domain = { 'x' : [ 0 , 1 ], 'y' : [ 0 , 1 ]},
title = { 'text' : f "P( { par } ↑)" },
gauge = {
'axis' : { 'range' : [ 0 , 100 ]},
'bar' : { 'color' : "#0f2742" },
'steps' : [
{ 'range' :[ 0 , 40 ], 'color' : '#d9534f' },
{ 'range' :[ 40 , 60 ], 'color' : '#f0ad4e' },
{ 'range' :[ 60 , 100 ], 'color' : '#2a9d8f' }
],
}
))
st . plotly_chart ( fig , use_container_width = True )
st . markdown ( "### Resumen contextual — versión simple" , unsafe_allow_html = True )
st . markdown ( f "<div class='card'><div class='explain'> { texto_simple . replace ( chr ( 10 ), '<br/>' ) } </div></div>" , unsafe_allow_html = True )
with st . expander ( "Ver explicación técnica (detalle para analistas)" ):
st . markdown ( f "<div class='card'><div class='explain'> { texto_tecnico . replace ( chr ( 10 ), '<br/>' ) } </div></div>" , unsafe_allow_html = True )
# ------------------ Tabla de componentes y coeficientes ------------------
st . markdown ( "### Componentes (valores normalizados) y coeficientes MAP" )
comp_df = pd . DataFrame ({
"Componente" : feature_cols ,
"Valor (normalizado)" : [ f " { v : .4f } " for v in x_live ],
"beta_MAP" : [ f " { float ( b ) : .4f } " for b in beta_map ],
"Contribución firmada (%)" : [ f " { float ( c ) : .1f } %" for c in contrib_pct ]
})
st . table ( comp_df )
# ------------------ Exportar resultado ------------------
out_df = pd . DataFrame ([{
"par" : par , "cot_long" : cot_long , "cot_short" : cot_short , "net" : net ,
"fed_prob" : fed_prob , "retail_pct" : retail_pct ,
"p_mean" : p_mean , "p_2.5" : p_low , "p_97.5" : p_high ,
2026-02-22 13:03:37 -05:00
"conviction" : conviction , "decision" : decision ,
"intraday_signal" : intraday_signal
2026-02-22 11:24:02 -05:00
}])
try :
excel_bytes = to_excel_bytes ( out_df )
st . download_button ( "Exportar resultado (Excel)" , data = excel_bytes , file_name = f "kennis_ { par . replace ( '/' , '' ) } _signal.xlsx" )
except Exception as e :
st . error ( "Error exportando Excel: " + str ( e ))
st . markdown ( "---" )
st . markdown ( "<div class='small'>Implementación: Logistic bayesiano táctico (MAP + Laplace). Suba CSV con 'target_up' para calibración real y backtests. Uso profesional: combine con price feed y motor de ejecución.</div>" , unsafe_allow_html = True )
st . markdown ( "© Kennis FX Markets — Institutional Bayesian Intelligence Engine" , unsafe_allow_html = True )