Update kennis_streamlit.py
This commit is contained in:
+48
-21
@@ -19,6 +19,7 @@ st.markdown(
|
|||||||
.explain { color:#22303a; font-size:14px; line-height:1.45; }
|
.explain { color:#22303a; font-size:14px; line-height:1.45; }
|
||||||
.small { font-size:12px; color:#6f8190; }
|
.small { font-size:12px; color:#6f8190; }
|
||||||
hr { border:none; border-top: 1px solid #e6eef8; }
|
hr { border:none; border-top: 1px solid #e6eef8; }
|
||||||
|
.warning { color:#8a6d3b; }
|
||||||
</style>
|
</style>
|
||||||
""",
|
""",
|
||||||
unsafe_allow_html=True,
|
unsafe_allow_html=True,
|
||||||
@@ -90,7 +91,6 @@ def fit_bayesian_logit(X, y, prior_var=0.5, maxiter=200, tol=1e-8):
|
|||||||
beta = beta + delta
|
beta = beta + delta
|
||||||
if np.linalg.norm(delta) < tol:
|
if np.linalg.norm(delta) < tol:
|
||||||
break
|
break
|
||||||
# final H may be singular in degenerate data; add jitter defensivo
|
|
||||||
try:
|
try:
|
||||||
cov = np.linalg.inv(H)
|
cov = np.linalg.inv(H)
|
||||||
except np.linalg.LinAlgError:
|
except np.linalg.LinAlgError:
|
||||||
@@ -104,6 +104,7 @@ def to_excel_bytes(df):
|
|||||||
return out.getvalue()
|
return out.getvalue()
|
||||||
|
|
||||||
# ------------------ Ingesta de datos ------------------
|
# ------------------ Ingesta de datos ------------------
|
||||||
|
using_demo = False
|
||||||
if uploaded is not None:
|
if uploaded is not None:
|
||||||
try:
|
try:
|
||||||
df = pd.read_csv(uploaded)
|
df = pd.read_csv(uploaded)
|
||||||
@@ -111,8 +112,9 @@ if uploaded is not None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error("Error leyendo CSV: " + str(e))
|
st.error("Error leyendo CSV: " + str(e))
|
||||||
df = pd.DataFrame()
|
df = pd.DataFrame()
|
||||||
|
using_demo = True
|
||||||
else:
|
else:
|
||||||
# demo sintético (multi-par no real) — solo para UI y pruebas
|
using_demo = True
|
||||||
np.random.seed(42)
|
np.random.seed(42)
|
||||||
N = 420
|
N = 420
|
||||||
cot_long_demo = np.random.normal(12000, 3000, size=N).astype(int)
|
cot_long_demo = np.random.normal(12000, 3000, size=N).astype(int)
|
||||||
@@ -156,9 +158,10 @@ if "target_up" in df_feat.columns:
|
|||||||
X_train = train_df[feature_cols].fillna(0).values
|
X_train = train_df[feature_cols].fillna(0).values
|
||||||
y_train = train_df["target_up"].astype(int).values
|
y_train = train_df["target_up"].astype(int).values
|
||||||
else:
|
else:
|
||||||
|
# Should not happen because demo includes target_up, but keep safe fallback
|
||||||
train_df = df_feat.copy()
|
train_df = df_feat.copy()
|
||||||
X_train = train_df[feature_cols].fillna(0).values
|
X_train = train_df[feature_cols].fillna(0).values
|
||||||
y_train = train_df["target_up"].astype(int).values
|
y_train = df_feat.get("target_up", pd.Series(np.zeros(len(train_df)))).astype(int).values
|
||||||
|
|
||||||
if retrain and uploaded is None:
|
if retrain and uploaded is None:
|
||||||
st.warning("Para reentrenar debe subir un CSV histórico con etiqueta 'target_up'.")
|
st.warning("Para reentrenar debe subir un CSV histórico con etiqueta 'target_up'.")
|
||||||
@@ -178,30 +181,47 @@ live_feat = build_features(aug, s_cot_local=s_cot).iloc[-1]
|
|||||||
x_live = live_feat[feature_cols].fillna(0).values
|
x_live = live_feat[feature_cols].fillna(0).values
|
||||||
net = float(live_feat["net"])
|
net = float(live_feat["net"])
|
||||||
|
|
||||||
# Posterior sampling + predicción (defensiva)
|
# Posterior sampling + predicción (defensiva) + calibración contra sesgo histórico
|
||||||
|
sample_probs_adj = None
|
||||||
try:
|
try:
|
||||||
# ensure cov_post is symmetric PSD (small jitter)
|
|
||||||
cov_post = np.array(cov_post)
|
cov_post = np.array(cov_post)
|
||||||
cov_post = 0.5 * (cov_post + cov_post.T)
|
cov_post = 0.5 * (cov_post + cov_post.T)
|
||||||
jitter = 1e-8 * np.eye(cov_post.shape[0])
|
jitter = 1e-8 * np.eye(cov_post.shape[0])
|
||||||
samples = np.random.multivariate_normal(beta_map, cov_post + jitter, size=4000)
|
samples = np.random.multivariate_normal(beta_map, cov_post + jitter, size=4000)
|
||||||
sample_probs = sigmoid(samples.dot(x_live))
|
sample_probs = sigmoid(samples.dot(x_live))
|
||||||
p_mean = float(sample_probs.mean())
|
|
||||||
p_low = float(np.percentile(sample_probs, 2.5))
|
# Calibración: centramos la predicción para neutralizar el sesgo medio en entrenamiento
|
||||||
p_high = float(np.percentile(sample_probs, 97.5))
|
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:
|
except Exception:
|
||||||
eta = float(x_live.dot(beta_map))
|
eta = float(x_live.dot(beta_map))
|
||||||
p_mean = float(sigmoid(eta))
|
p_mean = float(sigmoid(eta))
|
||||||
p_low, p_high = p_mean, p_mean
|
p_low, p_high = p_mean, p_mean
|
||||||
sample_probs = np.array([p_mean])
|
sample_probs_adj = np.array([p_mean])
|
||||||
|
calibration_applied = False
|
||||||
|
|
||||||
# Índice de convicción
|
# Índice de convicción (usando distribución calibrada si existe)
|
||||||
dispersion = max(1e-9, float(np.std(sample_probs)))
|
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 = 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)
|
conviction = round(conviction, 1)
|
||||||
|
|
||||||
# Decisión táctica
|
# Decisión táctica profesional (probabilidad + convicción)
|
||||||
if p_mean >= 0.60:
|
if conviction < 35:
|
||||||
|
decision = "ESPERAR (NO OPERAR)"
|
||||||
|
decision_flag = "wait"
|
||||||
|
elif p_mean >= 0.60:
|
||||||
decision = "COMPRAR (LONG)"
|
decision = "COMPRAR (LONG)"
|
||||||
decision_flag = "buy"
|
decision_flag = "buy"
|
||||||
elif p_mean <= 0.40:
|
elif p_mean <= 0.40:
|
||||||
@@ -219,12 +239,11 @@ else:
|
|||||||
contrib_pct = np.zeros_like(contrib_raw)
|
contrib_pct = np.zeros_like(contrib_raw)
|
||||||
|
|
||||||
# ------------------ Narrativa simplificada + técnica (en español) ------------------
|
# ------------------ 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):
|
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.
|
Versión legible para usuario promedio + explicación técnica desplegable.
|
||||||
Devuelve (texto_simple, texto_tecnico).
|
Devuelve (texto_simple, texto_tecnico).
|
||||||
"""
|
"""
|
||||||
# Frases simples y directas
|
|
||||||
prob_text = f"P({par} ↑) ≈ {p_mean*100:.1f}% (IC95%: {p_low*100:.1f}%–{p_high*100:.1f}%). Convicción: {conviction}/100."
|
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":
|
if decision_flag == "buy":
|
||||||
simple = (
|
simple = (
|
||||||
@@ -241,10 +260,10 @@ def narrative_tactical_simplificada(p_mean, p_low, p_high, conviction, decision_
|
|||||||
else:
|
else:
|
||||||
simple = (
|
simple = (
|
||||||
"Recomendación: **No operar (esperar)**.\n\n"
|
"Recomendación: **No operar (esperar)**.\n\n"
|
||||||
"Por qué (simple): la probabilidad está cerca de equilibrio y la incertidumbre es elevada; mejor esperar confirmación."
|
"Por qué (simple): la probabilidad está cerca de equilibrio o la convicción es baja; mejor esperar confirmación."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Construimos el texto técnico (audit-friendly) — compacto
|
# Texto técnico
|
||||||
fed_s = x_live[3]
|
fed_s = x_live[3]
|
||||||
cot_s = x_live[0]
|
cot_s = x_live[0]
|
||||||
retail_s = x_live[4]
|
retail_s = x_live[4]
|
||||||
@@ -252,6 +271,10 @@ def narrative_tactical_simplificada(p_mean, p_low, p_high, conviction, decision_
|
|||||||
tech_lines = []
|
tech_lines = []
|
||||||
tech_lines.append(f"Resumen técnico — activo: {par}")
|
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.")
|
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
|
# Macro
|
||||||
if fed_s > 0.4:
|
if fed_s > 0.4:
|
||||||
tech_lines.append("- Macro: FedWatch con sesgo hawkish → favorece USD fuerte en el corto plazo.")
|
tech_lines.append("- Macro: FedWatch con sesgo hawkish → favorece USD fuerte en el corto plazo.")
|
||||||
@@ -273,14 +296,14 @@ def narrative_tactical_simplificada(p_mean, p_low, p_high, conviction, decision_
|
|||||||
tech_lines.append("- Retail: minoristas inclinados a vender — puede reforzar momentum bajista si lo institucional confirma.")
|
tech_lines.append("- Retail: minoristas inclinados a vender — puede reforzar momentum bajista si lo institucional confirma.")
|
||||||
else:
|
else:
|
||||||
tech_lines.append("- Retail: posicionamiento minorista balanceado — no prevalece contrarian ahora.")
|
tech_lines.append("- Retail: posicionamiento minorista balanceado — no prevalece contrarian ahora.")
|
||||||
# Contribuciones (compactas)
|
# Contribuciones
|
||||||
tech_lines.append("- Descomposición de contribuciones (valores y % firmadas):")
|
tech_lines.append("- Descomposición de contribuciones (valores y % firmadas):")
|
||||||
for name, pct, val in zip(feature_cols, contrib_pct, x_live):
|
for name, pct, val in zip(feature_cols, contrib_pct, x_live):
|
||||||
sign = "+" if pct >= 0 else "-"
|
sign = "+" if pct >= 0 else "-"
|
||||||
tech_lines.append(f" • {name}: {sign}{abs(pct):.1f}% (valor {val:.3f})")
|
tech_lines.append(f" • {name}: {sign}{abs(pct):.1f}% (valor {val:.3f})")
|
||||||
# Razonamiento y ejecución
|
# Razonamiento
|
||||||
if decision_flag == "buy":
|
if decision_flag == "buy":
|
||||||
tech_lines.append("- Recomendación táctica: acumulación gradual LONG. Ejecución: scale-in 3 tramos; tamaño inicial 0.5–1% notional; stop 1.0–1.5×ATR; R:R ≥ 1:1.5.")
|
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":
|
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.")
|
tech_lines.append("- Recomendación táctica: considerar SHORT táctico o reducir largos. Ejecución: tamaño reducido; stops ajustados; vigilar noticias.")
|
||||||
else:
|
else:
|
||||||
@@ -293,7 +316,7 @@ def narrative_tactical_simplificada(p_mean, p_low, p_high, conviction, decision_
|
|||||||
|
|
||||||
# Obtener textos simple y técnico
|
# Obtener textos simple y técnico
|
||||||
texto_simple, texto_tecnico = narrative_tactical_simplificada(
|
texto_simple, texto_tecnico = narrative_tactical_simplificada(
|
||||||
p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols, par
|
p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols, par, calibration_applied, using_demo
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------ Interfaz: resultado y narrativa ------------------
|
# ------------------ Interfaz: resultado y narrativa ------------------
|
||||||
@@ -306,6 +329,10 @@ with left:
|
|||||||
unsafe_allow_html=True
|
unsafe_allow_html=True
|
||||||
)
|
)
|
||||||
st.markdown(f"<div class='muted'>Convicción: <strong>{conviction}/100</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("<hr/>", unsafe_allow_html=True)
|
||||||
st.markdown("### Plan táctico (resumen)", unsafe_allow_html=True)
|
st.markdown("### Plan táctico (resumen)", unsafe_allow_html=True)
|
||||||
if decision_flag == "buy":
|
if decision_flag == "buy":
|
||||||
|
|||||||
Reference in New Issue
Block a user