509 lines
23 KiB
Python
509 lines
23 KiB
Python
# kennis_streamlit.py
|
||
import streamlit as st
|
||
import numpy as np
|
||
import pandas as pd
|
||
from io import BytesIO
|
||
import plotly.graph_objects as go
|
||
|
||
# ------------------ Configuración de la página ------------------
|
||
st.set_page_config(page_title="Kennis FX Markets", layout="wide", initial_sidebar_state="expanded")
|
||
st.markdown(
|
||
"""
|
||
<style>
|
||
.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; }
|
||
.warning { color:#8a6d3b; }
|
||
</style>
|
||
""",
|
||
unsafe_allow_html=True,
|
||
)
|
||
|
||
# ------------------ Header / Hero ------------------
|
||
col1, col2 = st.columns([4,1])
|
||
with col1:
|
||
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)
|
||
st.markdown("<div class='muted'>Detection and strategic allocation guidance. (Edición táctica — swing / corto plazo)</div>", unsafe_allow_html=True)
|
||
with col2:
|
||
st.markdown("<div class='card'><div class='small'>Kennis FX — Tactical edition</div></div>", unsafe_allow_html=True)
|
||
|
||
st.markdown("---")
|
||
|
||
# ------------------ Sidebar: Inputs & opciones ------------------
|
||
st.sidebar.header("Entradas (táctica / swing)")
|
||
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"]
|
||
)
|
||
st.sidebar.markdown("O ingresa la fila live abajo:")
|
||
|
||
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
|
||
)
|
||
|
||
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)
|
||
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)
|
||
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("---")
|
||
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)")
|
||
|
||
# ------------------ Utilidades matemáticas ------------------
|
||
def sigmoid(x):
|
||
return 1.0 / (1.0 + np.exp(-x))
|
||
|
||
def fit_bayesian_logit(X, y, prior_var=0.5, maxiter=200, tol=1e-8):
|
||
n, p = X.shape
|
||
beta = np.zeros(p)
|
||
prior_prec = np.eye(p) / prior_var
|
||
H = np.eye(p) * 1e-6
|
||
for i in range(maxiter):
|
||
eta = X.dot(beta)
|
||
p_vec = sigmoid(eta)
|
||
W = p_vec * (1.0 - p_vec)
|
||
W = np.clip(W, 1e-8, None)
|
||
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)
|
||
except np.linalg.LinAlgError:
|
||
H += np.eye(p) * 1e-8
|
||
delta = np.linalg.solve(H, grad)
|
||
beta = beta + delta
|
||
if np.linalg.norm(delta) < tol:
|
||
break
|
||
try:
|
||
cov = np.linalg.inv(H)
|
||
except np.linalg.LinAlgError:
|
||
cov = np.linalg.pinv(H)
|
||
return beta, cov
|
||
|
||
def to_excel_bytes(df):
|
||
out = BytesIO()
|
||
with pd.ExcelWriter(out, engine="openpyxl") as writer:
|
||
df.to_excel(writer, index=False, sheet_name="signal")
|
||
return out.getvalue()
|
||
|
||
# ------------------ Ingesta de datos ------------------
|
||
using_demo = False
|
||
if uploaded is not None:
|
||
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()
|
||
using_demo = True
|
||
else:
|
||
using_demo = True
|
||
np.random.seed(42)
|
||
N = 420
|
||
cot_long_demo = np.random.normal(12000, 3000, size=N).astype(int)
|
||
cot_short_demo = np.random.normal(11000, 3000, size=N).astype(int)
|
||
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)
|
||
net_demo = cot_long_demo - cot_short_demo
|
||
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)
|
||
prob_demo = sigmoid(true_score)
|
||
target_demo = (np.random.rand(N) < prob_demo).astype(int)
|
||
df = pd.DataFrame({
|
||
"date": pd.date_range(end=pd.Timestamp.today(), periods=N),
|
||
"cot_long": cot_long_demo, "cot_short": cot_short_demo,
|
||
"fed_prob": fed_demo, "retail_pct": retail_demo,
|
||
"target_up": target_demo
|
||
})
|
||
st.info("Usando datos demo. Suba CSV histórico para calibración con datos reales.")
|
||
|
||
# ------------------ Feature engineering ------------------
|
||
def build_features(df_in, s_cot_local=50000):
|
||
df = df_in.copy()
|
||
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
|
||
|
||
df_feat = build_features(df, s_cot_local=s_cot)
|
||
|
||
# Features used in el modelo táctico
|
||
feature_cols = ["z_net", "z_delta_1w", "z_delta_4w", "signal_fed", "signal_retail"]
|
||
|
||
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:
|
||
# Should not happen because demo includes target_up, but keep safe fallback
|
||
train_df = df_feat.copy()
|
||
X_train = train_df[feature_cols].fillna(0).values
|
||
y_train = df_feat.get("target_up", pd.Series(np.zeros(len(train_df)))).astype(int).values
|
||
|
||
if retrain and uploaded is None:
|
||
st.warning("Para reentrenar debe subir un CSV histórico con etiqueta 'target_up'.")
|
||
|
||
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 ------------------
|
||
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]
|
||
x_live = live_feat[feature_cols].fillna(0).values
|
||
net = float(live_feat["net"])
|
||
|
||
# Posterior sampling + predicción (defensiva) + calibración contra sesgo histórico
|
||
sample_probs_adj = None
|
||
calibration_applied = False
|
||
try:
|
||
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)
|
||
sample_probs = sigmoid(samples.dot(x_live))
|
||
|
||
# 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
|
||
except Exception:
|
||
eta = float(x_live.dot(beta_map))
|
||
p_mean = float(sigmoid(eta))
|
||
p_low, p_high = p_mean, p_mean
|
||
sample_probs_adj = np.array([p_mean])
|
||
calibration_applied = False
|
||
|
||
# Índice de convicción (usando distribución calibrada si existe)
|
||
dispersion = max(1e-9, float(np.std(sample_probs_adj)))
|
||
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)
|
||
|
||
# Decisión táctica profesional (probabilidad + convicción)
|
||
if conviction < 35:
|
||
decision = "ESPERAR (NO OPERAR)"
|
||
decision_flag = "wait"
|
||
elif p_mean >= 0.60:
|
||
decision = "COMPRAR (LONG)"
|
||
decision_flag = "buy"
|
||
elif p_mean <= 0.40:
|
||
decision = "VENDER (SHORT)"
|
||
decision_flag = "sell"
|
||
else:
|
||
decision = "ESPERAR (NO OPERAR)"
|
||
decision_flag = "wait"
|
||
|
||
# Descomposición de contribuciones
|
||
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)
|
||
|
||
# ------------------ 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 = {}
|
||
|
||
# ------------------ Narrativa simplificada + técnica (en español) ------------------
|
||
def narrative_tactical_simplificada(p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols, par, calibration_applied, using_demo):
|
||
"""
|
||
Versión legible para usuario promedio + explicación técnica desplegable.
|
||
Devuelve (texto_simple, texto_tecnico).
|
||
"""
|
||
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"
|
||
"Por qué (simple): la probabilidad está cerca de equilibrio o la convicción es baja; mejor esperar confirmación."
|
||
)
|
||
|
||
# Texto técnico
|
||
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
|
||
|
||
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.")
|
||
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.")
|
||
# 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.")
|
||
# Contribuciones
|
||
tech_lines.append("- Descomposición de contribuciones (valores y % firmadas):")
|
||
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})")
|
||
# Razonamiento
|
||
if decision_flag == "buy":
|
||
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.")
|
||
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(
|
||
p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols, par, calibration_applied, using_demo
|
||
)
|
||
|
||
# ------------------ 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)
|
||
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)
|
||
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,
|
||
"conviction": conviction, "decision": decision,
|
||
"intraday_signal": intraday_signal
|
||
}])
|
||
|
||
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)
|