一、始まり:トランプが手描きするローソク足
ニュースの即時性は疑いようがありません。米国がイランを空爆したというニュースが流れると、原油価格は短時間で急騰しました。その過程で、トランプやイランの発言などの要因が絡み合い、互いに強化し合いながら相場を次々と新たな水準へと押し上げました。
よく冗談で「トランプがローソク足を手描きしている」と言います。価格が激しく変動する多くのケースは、テクニカル指標が作り出したものではなく、1件のツイート、1回のスピーチ、1回の政策発表が直接叩き出したものです。テクニカル分析は「今どこにいるのか」を教えてくれますが、ニュースこそが「なぜここにいるのか、次にどこへ行くのか」の重要な変数なのです。
ニュースの重要性は明らかですが、問題は現実的です。人間が24時間チャートとニュースを監視し続けることは不可能であり、情報の洪水の中で相場を動かす本当に重要なメッセージを瞬時に掴むことはさらに困難です。そこで、ごくシンプルなアイデアが浮かびます――ニュースをそのままローソク足チャートに「描き」、価格とメッセージを同じビューで同時に表示できないでしょうか。少なくとも「見える化」の問題を先に解決したいのです。
二、ニュースソースの選択:MCP経由で金十データに接続
ニュースを取り込むための第一歩は、更新が速く、構造がある程度標準化されたニュースソースを見つけることです。今回は金十データ(Jin10 Data)を採用し、MCP(Model Context Protocol)経由で接続し、list_flash(速報)とlist_news(ニュース)の2種類のインターフェースを呼び出します。
ここでは金十そのものに多くの紙幅を割くつもりはありません。これはあくまで現在使用している選択肢の一つであり、考え方と具体的なニュースソースは切り離されています。タイムスタンプ付きのタイトル/本文を提供し、同様のMCP標準方式で呼び出せるソースであれば、差し替え可能です。重要なのはこの接続層の設計であり、特定のツールに固定することではありません。
MCPの接続とセッション管理は、このシステムの中でも「基盤的」でありながら重要な部分です:
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)"))
セッションが確立された後、ニュースの取得は2回のツール呼び出しで行われ、さらに統一フォーマットへの正規化と重複除去を行います:
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
ニュースソースごとに返されるフィールド名はさまざまです(title/content/introduction、time/ts/created_at……)。そのため、中間層として _extract_items + _normalize を追加し、さまざまな形式をすべて {ts, time, title, source, full_text} という標準構造に統一します。以降のチャートやフィルタリングロジックは、データがどのインターフェースから来たかを意識する必要がなくなります。
注意: 利用にはMCPのAPI申請が必要です。
三、コア機能:ニュースをローソク足に「生やす」
ここからがこのツールの面白い部分です――ニュースとローソク足を同じチャートに配置します。
チャートに2つ目のseriesを追加し、タイプは flags で、ローソク足のseriesに重ねて「ニュースマーカーレイヤー」として機能させます。
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()
更新のたびに、まずKライン(ローソク足)データをインクリメンタルに更新し、次にキーワードでフィルタリングして「重要」なニュースを抽出し、それらを時間軸で対応するKラインのバーに合わせます:
python
def draw_chart(records):
global _last_bar_time, _last_news_hash, _flagged_news_ts
if not _chart or not records:
return
# ニュースに更新があるか検出し、あればチャートをリセットして再描画
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()
# series 0:Kライン、インクリメンタルに追加
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)
# series 1:キーワードに合致するニュースフラグ、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)
効果としては、チャート上に📰マークが現れたら、マウスを乗せると対応するニュースのタイトルが表示され、その位置はそのニュースが発生した時刻に対応するKラインです。価格の転換点とニュースのタイムポイントが、初めて直感的に同じ画面に表示されるようになりました——もはや「この部分はなぜか」を確認するために2つのウィンドウを行き来する必要はありません。
NEWS_KEYWORD は | で区切った複数のキーワード(例:「イラン|利上げ|雇用統計|関税」)に対応しており、システムはキーワードに合致するニュースを優先的にチャートにマークすることで、無関係な速報でチャートが埋め尽くされるのを防ぎます。
四、ステータスパネル:相場、ポジション、ニュースを1画面で確認
チャートに加えて、LogStatus を通じて出力される一連のステータステーブルも作成しました。これにはリアルタイム相場、口座残高と損益、現在の保有ポジション、キーワードに合致したニュース、そして最新の全量速報が含まれます:
python
def make_status(symbol, ticker, positions, equity):
# 4. キーワードニュース(出所ではなく、合致したキーワードを表示)
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 = [["-", "-", "キーワードに関連するニュースはありません"]]
さらに、簡単な手動指示インターフェース——買い建て、売り建て、買い決済、売り決済、一括全決済、注文数量の変更——はすべて 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))
全体を通して、FUSE は本質的に「情報統合 + 手動実行」のチャート監視パネルです。価格、ニュース、ポジション、口座状況をできるだけ同じ画面に集約し、意思決定は依然として完全に人間に委ねられています。代わりに判断するわけではありませんが、判断する際に見落としを減らすようサポートします。
五、限界:人間が依然として最大の変数
このバージョンの限界はかなり明白であり、私たちもそれを避けて通りたいとは思いません。
第一に、ニュースと価格の対応関係は「大まかな粒度」です。ニュースをタイムスタンプに従って対応するローソク足バーに貼り付けているだけで、内容レベルでの解釈は行っていません。あるニュースが好材料なのか悪材料なのか、相場を引き起こすかどうかは、完全に人間の判断に依存します。
第二に、キーワードフィルタリング自体は比較的単純な手法です。キーワードにヒットしたからといってニュースが本当に重要であるとは限らず、ヒットしなかったからといって重要でないとも限りません。その間には、個人の認知や経験、さらにはその日の体調などが大きなウェイトを占める可能性があります。同じニュースでも、異なる人間が同じチャートを見ていれば、まったく異なる結論に達する可能性があります。
第三に、プロセス全体が依然として「人間がループ内に存在する」ものであり、応答速度は人間の反応速度に制限されます。そして、多くの場合、相場のニュースに対する反応は分単位、さらには秒単位で発生します。
もしこの方向性にご興味があれば、今後は大規模言語モデルベースの自動化バージョンを開発し、ニュースの初期解釈や重要度判断をモデルに任せ、人間の意思決定を補助、あるいは代替することも試みます。ご関心がおありでしたら、引き続きご注目ください。
ストラテジーソースコード: リアルタイムニュース導火線システム
- 1





