在单台 VPS 上搭建日本股票分析服务:EDINET DB + Yahoo Finance + LLM

需求

手头有一台 VPS,已经跑了视频生成、语音转写等服务。现在想加一个股票分析功能:

  • 输入日本股票代码(如 4167)
  • 自动获取公司财务数据、股价
  • 输出结构化的分析报告(基本面、估值、风险、关注动态)

数据源选型

日本股票分析需要两类数据:财务数据股价数据

财务数据:EDINET DB

EDINET 是日本金融厅的电子披露系统,所有上市公司的有价证券报告书、决算短信都在这里。EDINET DB 是它的第三方 API 封装,提供 REST 接口。

端点用途
/v1/search?q={code}用证券代码搜索,获取 EDINET 代码
/v1/companies/{edinet_code}/earnings获取决算短信历史(实绩 + 通期预想)
/v1/companies/{edinet_code}/financials获取 FY2011-2025 的长期财务数据

免费版限制:100 请求/天,超限返回 429。对于分析和测试来说够用。

股价数据:Yahoo Finance

日本股票的股价可以用 yfinance 库获取,代码格式是 4167.T(加 .T 后缀)。

能拿到的字段包括:当前价、涨跌幅、市值、PER、PBR、股息率、52周高低。

注意:Yahoo Finance 的数据仅供个人使用,如果要做商用服务,需要换回 JPX 官方的 J-Quants API。

关键陷阱:代码格式不同

EDINET DB 和 Yahoo Finance 使用不同的公司标识符

数据源格式例子
EDINET DB(search 输入)4 位证券代码4167
EDINET DB(返回)E + 5 位E36086
Yahoo Finance4 位 + .T4167.T

脚本里先用 4167 搜索,拿到 E36086,再用它调 /earnings。股价则用 4167.T

完整脚本

核心逻辑分四步:

  1. 用证券代码搜索 EDINET 代码和公司信息
  2. 用 EDINET 代码获取最新决算短信
  3. 用 Yahoo Finance 获取股价
  4. 把数据组装成 prompt,交给 LLM 分析

python

import json
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

EDINET_KEY = "edb_你的Key"
LLM_URL = "http://172.17.0.1:3000/v1/chat/completions"
LLM_TOKEN = "sk-你的OneAPI令牌"
LLM_MODEL = "nvidia/nemotron-3-ultra-550b-a55b"
EDINET_BASE = "https://edinetdb.jp/v1"


def search_company(sec_code):
    r = requests.get(
        f"{EDINET_BASE}/search",
        params={"q": sec_code},
        headers={"X-API-Key": EDINET_KEY},
        timeout=30,
    )
    r.raise_for_status()
    data = r.json().get("data", [])
    return data[0] if data else None


def get_earnings(edinet_code):
    r = requests.get(
        f"{EDINET_BASE}/companies/{edinet_code}/earnings",
        headers={"X-API-Key": EDINET_KEY},
        timeout=30,
    )
    r.raise_for_status()
    return r.json().get("data", {}).get("earnings", [])


def get_stock_price(sec_code):
    import yfinance as yf
    ticker = yf.Ticker(f"{sec_code}.T")
    info = ticker.info
    return {
        "price": info.get("regularMarketPrice"),
        "market_cap": info.get("marketCap"),
        "trailing_pe": info.get("trailingPE"),
        "price_to_book": info.get("priceToBook"),
    }


@app.route("/analyze", methods=["POST"])
def analyze():
    data = request.get_json() or {}
    sec_code = str(data.get("code", "")).strip()

    # 1. 搜索公司
    company = search_company(sec_code)

    # 2. 决算短信
    earnings = get_earnings(company["edinet_code"])
    latest = earnings[0]

    # 3. 股价
    price = get_stock_price(sec_code)

    # 4. 计算进度率
    forecast_oi = latest.get("forecast_ordinary_income", 0)
    actual_oi = latest.get("ordinary_income", 0)
    progress = (actual_oi / forecast_oi * 100) if forecast_oi else 0

    # 5. LLM 分析
    prompt = f"""你是一位专业的日本股票分析师。
请基于以下数据,对 {company['name_ja']} 进行分析。

【最新季度实绩({latest['fiscal_year_end']} Q{latest['quarter']})】
- 売上高:{latest['revenue']} 百万円
- 営業利益:{latest.get('operating_income', 'N/A')} 百万円
- 経常利益:{latest.get('ordinary_income', 'N/A')} 百万円

【通期予想】
- 売上高:{latest.get('forecast_revenue', 'N/A')} 百万円
- 経常利益:{latest.get('forecast_ordinary_income', 'N/A')} 百万円

【進捗率】
- 経常利益進捗率:{progress:.1f}%

【財務状態】
- 自己資本比率:{latest.get('equity_ratio', 'N/A')}%

【最新股价】
{json.dumps(price, ensure_ascii=False, indent=2)}

请从以下四个维度输出中文分析报告:
1. 公司基本面分析
2. 通期予想达成可能性
3. 主要风险
4. 关注动态"""

    body = {
        "model": LLM_MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": 4096,
        "temperature": 0.3,
        "stream": False,
    }
    r = requests.post(
        LLM_URL,
        headers={"Authorization": f"Bearer {LLM_TOKEN}"},
        json=body,
        timeout=600,
    )
    analysis = r.json()["choices"][0]["message"]["content"]

    return jsonify({
        "code": sec_code,
        "company": company["name_ja"],
        "latest_quarter": f"{latest['fiscal_year_end']} Q{latest['quarter']}",
        "revenue": latest.get("revenue"),
        "ordinary_income": actual_oi,
        "forecast_ordinary_income": forecast_oi,
        "progress_percent": round(progress, 2),
        "stock_price": price.get("price"),
        "analysis": analysis,
    })

运行

bash

python stock_service.py

服务监听在 5002 端口。

测试

bash

curl -X POST http://localhost:5002/analyze \
  -H "Content-Type: application/json" \
  -d '{"code": "4167"}'

返回完整的 JSON 报告,耗时 2-3 分钟(主要是 LLM 推理)。

实测:4167 的分析输出

输入 4167(株式会社ココペリ),LLM 输出的报告要点:

基本面:Q1 营收 4.82 亿日元(+9.3%),但营业利润率只有 1.87%,经常利润率 1.45%——处于”微利/投资期”,规模效应还没显现。

通期预想达成难度:全年经常利润目标 8,200 万,Q1 只完成 700 万,进度率 8.5%,远低于均匀分布的 25%。剩余三季度需要单季平均 2,500 万,是 Q1 的 3.57 倍

核心风险

  • 盈利兑现不确定性(5 星)
  • 微盘股流动性陷阱(4 星)
  • 行业竞争与模式风险(3 星)

下一节点:2026 年 11 月的 Q2 财报,关键验证指标是上半期经常利润进度率是否 >35%。

缓存优化

LLM 分析需要 2-3 分钟,而且每次调用消耗 2 次 EDINET 配额(search + earnings)。免费版每天只有 100 次请求,重复分析同一只股票很浪费。

缓存设计

按日期缓存,同一天内重复请求直接返回已生成的结果:

python

from datetime import datetime
from pathlib import Path

CACHE_DIR = Path("/opt/rag-nvidia/cache")
CACHE_DIR.mkdir(exist_ok=True)


def get_cache_path(sec_code):
    today = datetime.now().strftime("%Y-%m-%d")
    return CACHE_DIR / f"{sec_code}_{today}.json"


def load_cache(sec_code):
    path = get_cache_path(sec_code)
    if path.exists():
        return json.loads(path.read_text(encoding="utf-8"))
    return None


def save_cache(sec_code, data):
    path = get_cache_path(sec_code)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")

在 /analyze 端点开头加缓存检查:

python

if not force_refresh:
    cached = load_cache(sec_code)
    if cached:
        cached["cached"] = True
        return jsonify(cached)

缓存效果

调用cached耗时
第一次False2-3 分钟
第二次True0.041 秒

第二次直接返回缓存,不消耗 EDINET 配额,也不调用 LLM。

强制刷新

如果需要重新分析(比如股价变动),传 force_refresh

bash

curl -X POST http://localhost:5002/analyze \
  -H "Content-Type: application/json" \
  -d '{"code": "4167", "force_refresh": true}'

踩坑记录

1. EDINET DB 和 Yahoo Finance 的代码格式不同
EDINET 用 E36086,Yahoo 用 4167.T。脚本里需要做转换。

2. LLM 输出是日文
即使 prompt 要求”中文输出”,nemotron-3-ultra 有时会跟随数据的语言输出日文。需要在 prompt 末尾强调”请用中文输出”。

3. 推理模型响应慢
nemotron-3-ultra 是推理模型,会先生成”思考过程”,再输出正式回答。2-3 分钟的等待是正常的。如果想加速,可以换非推理模型。

4. EDINET 免费版配额有限
100 请求/天,每次分析消耗 2 次(search + earnings)。每天约能分析 50 只股票。缓存能大幅减少配额消耗。

5. 部分数据缺失
EDINET 的 1Q 记录里可能没有 net_income 字段(公司只披露 EPS)。LLM 会正确标注”数据缺失”,不会编造。

当前状态

组件状态
EDINET DB 搜索
EDINET DB 决算短信
Yahoo Finance 股价
进度率自动计算
LLM 分析
报告缓存✅ 按日期缓存
Flask 服务✅ 运行在 5002

下一步

  1. 接入 n8n:定时批量分析多只关注的股票,结果发 Telegram
  2. 加历史趋势:调用 EDINET DB 的 /financials 端点,获取 FY2011-2025 的长期数据
  3. 存入 WordPress:把分析报告自动发布到站点
  4. 多股票对比:同时分析多只股票,生成行业对比报告

追记:
如何调查其他公司的业绩

服务搭好后,调查任意日本上市公司的业绩只需要把股票代码换掉

最简单的方式:curl 调用

bash

curl -s -X POST http://localhost:5002/analyze \
  -H "Content-Type: application/json" \
  -d '{"code": "7203"}' | python3 -c "
import sys, json
d = json.load(sys.stdin)
if 'error' in d:
    print('错误:', d['error'])
else:
    print(f\"{d['company']} ({d['code']})\")
    print(f\"最新季度: {d['latest_quarter']}\")
    print(f\"股价: {d['stock_price']} 円\")
    print(f\"营收: {d['revenue']} 百万円\")
    print(f\"经常利润: {d['ordinary_income']} 百万円\")
    print(f\"通期预想: {d['forecast_ordinary_income']} 百万円\")
    print(f\"进度率: {d['progress_percent']}%\")
    print(f\"缓存: {d.get('cached', False)}\")
    print()
    print(d['analysis'])
"

把 7203 换成目标公司的 4 位证券代码。首次调用约 2-3 分钟(LLM 分析),同一天内再次查同一只股票秒回(走缓存)。

常见日本公司代码

代码公司
7203トヨタ自動車(丰田)
6758ソニーグループ(索尼)
9984ソフトバンクグループ
6861キーエンス
8306三菱UFJフィナンシャル・グループ
9432日本電信電話(NTT)
4063信越化学工業
6501日立製作所

不知道代码怎么办

方法一:用 EDINET DB 搜索公司名

bash

curl -s -H "X-API-Key: edb_你的Key" \
  "https://edinetdb.jp/v1/search?q=トヨタ" | python3 -m json.tool --no-ensure-ascii

搜索支持日文和英文公司名。返回结果里的 sec_code 字段就是证券代码。

方法二:在 Yahoo Finance 上搜

访问 finance.yahoo.com 搜索公司名,URL 里会显示代码(如 7203.T),去掉 .T 就是 4 位代码。

批量调查多只股票

如果要一次查多只,写一个循环脚本:

bash

cat > /opt/rag-nvidia/batch_analyze.sh << 'ENDOFFILE'
#!/bin/bash
CODES=("7203" "6758" "9984" "6861")
for code in "${CODES[@]}"; do
    echo "================================"
    echo "分析 $code ..."
    curl -s -X POST http://localhost:5002/analyze \
      -H "Content-Type: application/json" \
      -d "{\"code\": \"$code\"}" | python3 -c "
import sys, json
d = json.load(sys.stdin)
if 'error' in d:
    print('  错误:', d['error'])
else:
    print(f\"  {d['company']} ({d['code']})\")
    print(f\"  股价: {d['stock_price']} 円 | 进度率: {d['progress_percent']}%\")
    print(f\"  报告已保存: {d['report_path']}\")
"
    sleep 2
done
ENDOFFILE

chmod +x /opt/rag-nvidia/batch_analyze.sh
/opt/rag-nvidia/batch_analyze.sh

配额注意:每只股票消耗 2 次 EDINET 请求(search + earnings)。4 只就是 8 次。免费版每天 100 次,约能分析 50 只。

配额与缓存

项目说明
EDINET DB 免费版100 请求/天
每次分析消耗2 次(search + earnings)
缓存效果同一天内重复查同一只股票不消耗配额
超限返回HTTP 429,等第二天重置

报告保存位置

每次分析的报告都保存到:

text

/opt/rag-nvidia/reports/{代码}_analysis.md

比如查了 7203,报告就在 /opt/rag-nvidia/reports/7203_analysis.md。直接 cat 就能看:

bash

cat /opt/rag-nvidia/reports/7203_analysis.md

建议的用法

先查一只你熟悉的公司,验证分析质量。比如已经查过 4167(ココペリ),可以再查 7203(丰田),对比 LLM 对大公司和小公司的分析差异。

bash

curl -s -X POST http://localhost:5002/analyze \
  -H "Content-Type: application/json" \
  -d '{"code": "7203"}' | python3 -c "
import sys, json
print(json.load(sys.stdin)['analysis'])
"

查完几家公司后,可以在 /opt/rag-nvidia/reports/ 目录下对比不同公司的报告,快速了解同行业或不同行业的业绩特征。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注