1. Inicio: Trump dibuja velas a mano
La inmediatez de las noticias es indiscutible. Tras la noticia del ataque aéreo estadounidense contra Irán, el precio del crudo subió bruscamente en poco tiempo. Durante este proceso, factores como las declaraciones de Trump e Irán se entrelazaron y reforzaron mutuamente, empujando la cotización a nuevos rangos tras otro.
A menudo bromeamos diciendo que "Trump dibuja velas a mano": muchas veces, las fuertes fluctuaciones del precio no se deben a la evolución de los indicadores técnicos, sino que son directamente provocadas por un tuit, un discurso o una declaración política. El análisis técnico te dice "dónde estás ahora", pero las noticias suelen ser la clave para entender "por qué estás aquí y hacia dónde podrías ir a continuación".
La importancia de las noticias está fuera de duda, pero el problema es real: una persona no puede monitorear pantallas y noticias 24/7, y mucho menos captar al instante, en medio de un torrente de información, la noticia que realmente moverá el mercado. Así surge una idea sencilla: ¿podríamos "dibujar" las noticias directamente sobre el gráfico de velas, mostrando precio y noticias en la misma vista? Al menos, resolver primero el problema de "ver".
2. Elección de la fuente de noticias: acceso a Jin10 mediante MCP
Para integrar las noticias, el primer paso es encontrar una fuente que se actualice rápidamente y tenga una estructura relativamente estándar. En esta ocasión, hemos optado por Jin10 Data, accediendo a través del Protocolo de Contexto de Modelo (MCP, por sus siglas en inglés), invocando dos tipos de interfaces: list_flash (flash) y list_news (noticias).
No dedicaré mucho espacio a presentar Jin10 en sí; es solo una opción que utilizamos actualmente. La idea y la fuente de noticias específica están desacopladas: mientras una fuente pueda proporcionar títulos/cuerpos con marca de tiempo y se pueda invocar de manera estándar similar a MCP, se puede reemplazar. Lo importante es el diseño de esta capa de integración, no la vinculación a una herramienta concreta.
La conexión y gestión de sesiones de MCP son partes bastante "básicas" pero cruciales de este sistema:
python
def _mcp_post(payload, is_notification=False):
global _mcp_session_id, _mcp_req_id
if not is_notification:
_mcp_req_id += 1
payload["id"] = _mcp_req_id
body = json.dumps(payload, ensure_ascii=False).encode()
req = urllib.request.Request(
JIN10_MCP_URL, data=body, headers=_mcp_headers(), method="POST"
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
sid = resp.headers.get("Mcp-Session-Id")
if sid:
_mcp_session_id = sid
if resp.status == 202:
return {}
text = resp.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as e:
raise RuntimeError("HTTP %d: %s" % (e.code, e.read().decode()[:400]))
except urllib.error.URLError as e:
raise RuntimeError("Network: " + str(e))
return _mcp_parse(text)
def mcp_init():
global _mcp_ready
mcp_rpc("initialize", {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "fuse-fmz", "version": "1.0"},
})
mcp_notify("notifications/initialized")
_mcp_ready = True
Log("MCP ready session_id=" + (_mcp_session_id or "(none)"))
Una vez establecida la sesión, obtener las noticias implica dos llamadas a herramientas, seguidas de una normalización y deduplicación uniforme del formato:
python
def refresh_news():
global _cached_news, _last_news_at, _mcp_ready
if not JIN10_MCP_TOKEN:
return
now = int(time.time())
if now - _last_news_at < NEWS_REFRESH_SEC and _cached_news:
return
_last_news_at = now
try:
if not _mcp_ready:
mcp_init()
flash_raw = mcp_call_tool("list_flash")
news_raw = mcp_call_tool("list_news")
combined = (
_normalize(_extract_items(flash_raw), "flash") +
_normalize(_extract_items(news_raw), "news")
)
combined.sort(key=lambda x: x["ts"], reverse=True)
_cached_news = combined[:80]
Log("News updated: %d items" % len(_cached_news))
except Exception as e:
Log("News refresh failed: " + str(e))
_mcp_ready = False
Las diferentes fuentes de noticias devuelven nombres de campo muy variados (title/content/introduction, time/ts/created_at...), por lo que se añade una capa intermedia _extract_items + _normalize para unificar todos los formatos en una estructura estándar {ts, time, title, source, full_text}, de modo que la lógica de gráficos y filtrado posterior no necesite saber de qué interfaz provienen los datos.
Nota: Se requiere solicitar la API de MCP para su uso.
3. Mecánica central: hacer que las noticias "crezcan" en las velas
Ahora viene la parte realmente interesante de esta herramienta: colocar las noticias y las velas en el mismo gráfico.
Hemos añadido una segunda serie en el gráfico, de tipo flags, vinculada a la serie de velas, que actúa como "capa de marcadores de noticias":
python
def init_chart(symbol):
global _chart
_chart = Chart({
"__isStock": True,
"chart": {"style": {"fontFamily": "Microsoft YaHei, SimHei, Arial, sans-serif"}},
"title": {"text": "FUSE " + symbol},
"xAxis": {"type": "datetime"},
"series": [
{
"id": "kline",
"type": "candlestick",
"name": symbol,
"data": [],
},
{
"type": "flags",
"name": "News",
"onSeries": "kline",
"shape": "circlepin",
"color": "#F59E0B",
"fillColor": "#F59E0B",
"width": 16,
"data": [],
},
],
})
_chart.reset()
En cada actualización, primero se actualizan incrementalmente los datos de las velas K, luego se filtran las noticias "importantes" según palabras clave y se alinean temporalmente con la barra K correspondiente:
python
def draw_chart(records):
global _last_bar_time, _last_news_hash, _flagged_news_ts
if not _chart or not records:
return
# Detecta si hay actualizaciones en las noticias; si las hay, reinicia el gráfico y redibuja
news_hash = hash(tuple(n.get("ts", 0) for n in _cached_news[:10]))
news_changed = (news_hash != _last_news_hash)
if news_changed:
_chart.reset()
_last_bar_time = 0
_last_news_hash = news_hash
_flagged_news_ts = set()
# serie 0: velas K, añadir incrementalmente
for r in records:
t = r['Time']
bar = [t, r['Open'], r['High'], r['Low'], r['Close']]
if t > _last_bar_time:
_chart.add(0, bar)
_last_bar_time = t
elif t == _last_bar_time:
_chart.add(0, bar, -1)
# serie 1: bandera de noticias por palabra clave, alineada con el tiempo de la barra K
if not _cached_news:
return
kws = [k.strip() for k in NEWS_KEYWORD.split("|") if k.strip()]
kw_news = [n for n in _cached_news if not kws or any(k in n.get("full_text", n["title"]) for k in kws)]
if not kw_news:
return
p_ms = PERIOD_MS.get(KLINE_PERIOD, 60000)
first = records[0]['Time']
last = records[-1]['Time']
by_bar = {}
for n in kw_news:
if not n.get("ts"):
continue
key = (n["ts"] // p_ms) * p_ms
if key not in by_bar:
by_bar[key] = n
for ts, item in sorted(by_bar.items()):
if not (first <= ts <= last):
continue
if ts in _flagged_news_ts:
continue
_chart.add(1, {
"x": ts,
"title": "📰",
"text": item["title"][:100],
})
_flagged_news_ts.add(ts)
El resultado es: cada vez que aparece un marcador 📰 en el gráfico, al pasar el ratón sobre él se puede ver el titular de la noticia correspondiente, y su ubicación coincide exactamente con la vela K del momento en que ocurrió la noticia. Los puntos de inflexión de los precios y los momentos de las noticias se presentan por primera vez de manera intuitiva en la misma imagen, eliminando la necesidad de cambiar entre dos ventanas para entender "qué causó este tramo".
NEWS_KEYWORD admite múltiples palabras clave separadas por | (por ejemplo, "Irán|subida de tipos|no agrícolas|aranceles"). El sistema priorizará marcar las noticias que contengan estas palabras clave en el gráfico, evitando que noticias irrelevantes saturen la pantalla.
IV. Panel de estado: cotización, posiciones, noticias en una sola pantalla
Además del gráfico, hemos creado un conjunto de tablas de estado que se muestran mediante LogStatus, que incluyen cotización en tiempo real, capital y ganancias/pérdidas de la cuenta, posiciones actuales, noticias que coinciden con palabras clave y los últimos titulares completos:
python
def make_status(symbol, ticker, positions, equity):
# 4. Noticias por palabra clave (mostrar la palabra clave encontrada, no la fuente)
kws = [k.strip() for k in NEWS_KEYWORD.split("|") if k.strip()]
kw_rows = []
for item in _cached_news[:40]:
t = item.get("time") or (_D(item["ts"]) if item.get("ts") else "-")
title = item["title"][:90]
text = item.get("full_text", item["title"])
hit_kws = [k for k in kws if k in text]
if hit_kws:
kw_rows.append([t, "/".join(hit_kws), title])
if not kw_rows:
kw_rows = [["-", "-", "Sin noticias relacionadas con palabras clave"]]
Además, se ha añadido una interfaz de comandos manuales simple: abrir largo, abrir corto, cerrar largo, cerrar corto, cerrar todo con un clic y modificar la cantidad de la orden, todo mediante GetCommand():
python
def handle_command(symbol):
global _cur_amount, _last_news_at
cmd = GetCommand()
if not cmd:
return
Log("CMD: " + cmd)
parts = cmd.split(":")
key = parts[0]
val = parts[1] if len(parts) > 1 else ""
if key == "openLong": market_order(symbol, "openLong", _cur_amount)
elif key == "openShort": market_order(symbol, "openShort", _cur_amount)
elif key == "closeLong": market_order(symbol, "closeLong", _cur_amount)
elif key == "closeShort": market_order(symbol, "closeShort", _cur_amount)
elif key == "closeAll": close_all(symbol)
elif key == "amount":
_cur_amount = float(val)
Log("Amount updated: " + str(_cur_amount))
En conjunto, FUSE es esencialmente un panel de monitoreo que combina "información integrada + ejecución manual": reúne precios, noticias, posiciones y estado de la cuenta en la misma pantalla tanto como sea posible. La decisión sigue recayendo completamente en el humano — no juzga por ti, pero intenta que no te pierdas demasiadas cosas al momento de decidir.
V. Limitaciones: El ser humano sigue siendo la mayor variable
Las limitaciones de esta versión son bastante evidentes y no queremos evadirlas.
Primero, la correspondencia entre noticias y precios es de "granularidad gruesa" — simplemente cuelga las noticias en la vela del K-line correspondiente según su marca de tiempo, sin realizar una interpretación a nivel de contenido. Depende completamente del humano juzgar si una noticia es positiva o negativa, y si desencadenará movimientos en el mercado.
Segundo, el filtrado por palabras clave es en sí mismo un enfoque bastante básico. Que una noticia cumpla con las palabras clave no significa que sea realmente importante, y que no las cumpla tampoco significa que no lo sea. En este proceso, la percepción, experiencia e incluso el estado de ánimo del individuo ese día pueden pesar mucho — ante la misma noticia, diferentes personas mirando el mismo gráfico pueden llegar a conclusiones completamente distintas.
Tercero, todo el flujo sigue siendo "humano en el circuito", por lo que la velocidad de respuesta está limitada por la reacción humana, y en muchos casos la reacción del mercado a las noticias ocurre en minutos o incluso segundos.
Si les interesa esta dirección, en el futuro intentaremos crear una versión automatizada basada en un modelo grande, donde el modelo realice una interpretación preliminar de las noticias y evalúe su importancia, sirviendo como apoyo o incluso reemplazo de la decisión humana. Si están interesados, pueden seguirnos.
Código fuente de la estrategia: Sistema de detonador de noticias en tiempo real
- 1




