构建一个高质量的股票报告生成器:使用 Node.js、Express 和 OpenAI API

731 阅读7分钟

在本文中,我们将详细介绍如何使用 Node.js、Express 和 OpenAI API 构建一个专业级的股票报告生成器。该应用程序将获取股票数据,执行情感和行业分析,最终生成一份全面的投资报告。此外,您可以访问我们的在线网站进行体验:Stock Report Pro

目录

  1. 项目概述
  2. 环境设置
  3. 构建 Express 服务器
  4. 数据获取与处理
  5. 与 OpenAI API 集成
  6. 生成最终报告
  7. 测试应用程序
  8. 总结

项目概述

我们的目标是创建一个 API 端点,给定股票代码后,生成一份详细的投资报告。报告将包括:

  • 公司概览
  • 财务表现
  • 管理层讨论与分析(MDA)
  • 情感分析
  • 行业分析
  • 风险与机会
  • 投资建议

我们将利用外部 API 获取股票数据,并使用 OpenAI API 执行高级分析。

环境设置

前提条件

  • 安装了 Node.js
  • 拥有 OpenAI API 密钥(如果没有,请在 OpenAI 注册)

初始化项目

创建新目录并初始化 Node.js 项目:

mkdir stock-report-generator
cd stock-report-generator
npm init -y

安装所需依赖项:

npm install express axios

创建项目结构:

mkdir routes utils data
touch app.js routes/report.js utils/helpers.js

构建 Express 服务器

设置 app.js

// app.js
const express = require('express');
const reportRouter = require('./routes/report');

const app = express();

app.use(express.json());
app.use('/api', reportRouter);

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
  • Express 初始化:导入 Express 并初始化应用程序。
  • 中间件:使用 express.json() 解析 JSON 请求体。
  • 路由:在 /api 挂载报告路由。
  • 服务器监听:在指定端口启动服务器。

数据获取与处理

创建辅助函数

utils/helpers.js 中,我们将定义用于数据获取和处理的实用函数。

// utils/helpers.js
const axios = require('axios');
const fs = require('fs');
const path = require('path');

const BASE_URL = 'https://your-data-api.com'; // 请替换为实际数据 API

/**
 * 获取上年度的起始和结束日期。
 * @returns {object} - 包含 `start` 和 `end` 日期的对象。
 */
function getLastYearDates() {
  const now = new Date();
  const end = now.toISOString().split('T')[0];
  now.setFullYear(now.getFullYear() - 1);
  const start = now.toISOString().split('T')[0];
  return { start, end };
}

/**
 * 将对象转换为字符串,排除指定的键。
 * @param {object} obj - 要转换的对象。
 * @param {string[]} excludeKeys - 要排除的键。
 * @returns {string} - 转换后的字符串。
 */
function objectToString(obj, excludeKeys = []) {
  return Object.entries(obj)
    .filter(([key]) => !excludeKeys.includes(key))
    .map(([key, value]) => `${key}: ${value}`)
    .join('\n');
}

/**
 * 从指定的端点获取数据。
 * @param {string} endpoint - API 端点。
 * @param {object} params - 查询参数。
 * @param {any} defaultValue - 请求失败时的默认值。
 * @returns {Promise<any>} - 获取的数据或默认值。
 */
async function fetchData(endpoint, params = {}, defaultValue = null) {
  try {
    const response = await axios.get(`${BASE_URL}${endpoint}`, { params });
    return response.data || defaultValue;
  } catch (error) {
    console.error(`Error fetching data from ${endpoint}:`, error.message);
    return defaultValue;
  }
}

/**
 * 从本地 JSON 文件读取数据。
 * @param {string} fileName - JSON 文件名。
 * @returns {any} - 读取的数据。
 */
function readLocalJson(fileName) {
  const filePath = path.join(__dirname, '../data', fileName);
  const data = fs.readFileSync(filePath, 'utf-8');
  return JSON.parse(data);
}

module.exports = {
  fetchData,
  objectToString,
  getLastYearDates,
  readLocalJson,
};
  • getLastYearDates:获取上一年的起始和结束日期。
  • objectToString:将对象转换为可读字符串,便于日志记录或准备提示信息。
  • fetchData:处理对外部 API 的 GET 请求,返回数据或默认值。
  • readLocalJson:从本地 JSON 文件读取数据。

实现股票数据获取

routes/report.js 中,我们将定义获取股票数据的函数。

// routes/report.js
const express = require('express');
const {
  fetchData,
  objectToString,
  getLastYearDates,
  readLocalJson,
} = require('../utils/helpers');

const router = express.Router();

/**
 * 获取股票数据,包括历史价格、财务信息、MDA 和主营业务信息。
 * @param {string} ticker - 股票代码。
 * @returns {Promise<object>} - 包含所有获取的数据的对象。
 */
async function fetchStockData(ticker) {
  try {
    const dates = getLastYearDates();
    const [historicalData, financialData, mdaData, businessData] = await Promise.all([
      fetchData('/stock_zh_a_hist', {
        symbol: ticker,
        period: 'weekly',
        start_date: dates.start,
        end_date: dates.end,
      }, []),

      fetchData('/stock_financial_benefit_ths', {
        code: ticker,
        indicator: '按年度',
      }, [{}]),

      fetchData('/stock_mda', { code: ticker }, []),

      fetchData('/stock_main_business', { code: ticker }, []),
    ]);

    const hist = historicalData[historicalData.length - 1];
    const currentPrice = (hist ? hist['开盘'] : 'N/A') + ' CNY';
    const historical = historicalData
      .map((item) => objectToString(item, ['股票代码']))
      .join('\n----------\n');

    const zsfzJson = readLocalJson('zcfz.json');
    const balanceSheet = objectToString(zsfzJson.find((item) => item['股票代码'] === ticker));

    const financial = objectToString(financialData[0]);

    const mda = mdaData.map(item => `${item['报告期']}\n${item['内容']}`).join('\n-----------\n');

    const mainBusiness = businessData.map(item =>
      `主营业务: ${item['主营业务']}\n产品名称: ${item['产品名称']}\n产品类型: ${item['产品类型']}\n经营范围: ${item['经营范围']}`
    ).join('\n-----------\n');

    return { currentPrice, historical, balanceSheet, mda, mainBusiness, financial };
  } catch (error) {
    console.error('Error fetching stock data:', error.message);
    throw error;
  }
}
  • fetchStockData:并发地获取多个数据,并处理结果。
  • 数据处理:格式化和转换数据,便于后续使用。
  • 错误处理:记录错误并重新抛出,以便在更高层级处理。

与 OpenAI API 集成

OpenAI API 交互函数

const axios = require('axios');

const OPENAI_API_KEY = 'your-openai-api-key'; // 请替换为您的 OpenAI API 密钥

/**
 * 与 OpenAI API 交互,获取完成结果。
 * @param {array} messages - 消息数组,包含系统提示和用户消息。
 * @returns {Promise<string>} - AI 的响应。
 */
async function analyzeWithOpenAI(messages) {
  try {
    const headers = {
      'Authorization': `Bearer ${OPENAI_API_KEY}`,
      'Content-Type': 'application/json',
    };
    const requestData = {
      model: 'gpt-4',
      temperature: 0.3,
      messages: messages,
    };

    const response = await axios.post(
      'https://api.openai.com/v1/chat/completions',
      requestData,
      { headers }
    );
    return response.data.choices[0].message.content.trim();
  } catch (error) {
    console.error('Error fetching analysis from OpenAI:', error.message);
    throw error;
  }
}
  • analyzeWithOpenAI:处理与 OpenAI API 的通信。
  • API 配置:设置模型、温度等参数。
  • 错误处理:记录错误并抛出。

执行情感分析

/**
 * 使用 OpenAI API 对新闻文章执行情感分析。
 * @param {string} ticker - 股票代码。
 * @returns {Promise<string>} - 情感分析摘要。
 */
async function performSentimentAnalysis(ticker) {
  const systemPrompt = `You are a sentiment analysis assistant. Analyze the sentiment of the given news articles for ${ticker} and provide a summary of the overall sentiment and any notable changes over time. Be measured and discerning. You are a skeptical investor.`;

  const tickerNewsResponse = await fetchData('/stock_news_specific', { code: ticker }, []);

  const newsText = tickerNewsResponse
    .map(item => `${item['文章来源']} Date: ${item['发布时间']}\n${item['新闻内容']}`)
    .join('\n----------\n');

  const messages = [
    { role: 'system', content: systemPrompt },
    {
      role: 'user',
      content: `News articles for ${ticker}:\n${newsText || 'N/A'}\n----\nProvide a summary of the overall sentiment and any notable changes over time.`,
    },
  ];

  return await analyzeWithOpenAI(messages);
}
  • performSentimentAnalysis:构建提示信息,调用 OpenAI API 进行分析。
  • 提示信息设计:确保提示信息清晰,包含必要的上下文。

分析行业

/**
 * 使用 OpenAI API 分析行业信息。
 * @param {string} industry - 行业名称。
 * @returns {Promise<string>} - 行业分析摘要。
 */
async function analyzeIndustry(industry) {
  const industryNewsResponse = await fetchData('/stock_news_specific', { code: industry }, []);
  const industryNewsArticles = industryNewsResponse
    .map(item => `${item['文章来源']} Date: ${item['发布时间']}\n${item['新闻内容']}`)
    .join('\n----------\n');

  const systemPrompt = `You are an industry analysis assistant. Provide an analysis of the ${industry} industry, including trends, growth prospects, regulatory changes, and competitive landscape.

Consider the following recent news articles relevant to the ${industry} industry:
${industryNewsArticles || 'N/A'}

Be measured and discerning. Truly think about the positives and negatives of the industry. Be sure of your analysis. You are a skeptical investor.`;

  const messages = [
    { role: 'system', content: systemPrompt },
    {
      role: 'user',
      content: `Provide an analysis of the ${industry} industry, taking into account the recent news articles provided.`,
    },
  ];

  return await analyzeWithOpenAI(messages);
}
  • analyzeIndustry:类似于情感分析,但侧重于更广泛的行业背景。

生成最终报告

编译所有数据

/**
 * 生成最终的投资报告。
 * @param {string} ticker - 股票代码。
 * @param {object} data - 收集的数据和分析结果。
 * @returns {Promise<string>} - 最终报告。
 */
async function provideFinalAnalysis(ticker, data) {
  const {
    sentimentAnalysis,
    industryAnalysis,
    balanceSheet,
    mda,
    mainBusiness,
    financial,
    currentPrice,
    historical,
  } = data;

  const systemPrompt = `You are a financial analyst tasked with providing a comprehensive investment recommendation for the stock ${ticker}. Your analysis should utilize the available data and aim for a length of approximately 1500 to 3000 words. Focus on the following key aspects:

1. **Company Overview**: Provide a brief overview of the company, including its business model, core products/services, and market position. Refer to the provided main business data.

2. **Financial Performance**:
   - Analyze key financial metrics based on the available data, including the current price (${currentPrice}), historical data, financial data, and balance sheet.
   - Highlight any significant financial trends and changes.

3. **Management Discussion and Analysis (MDA)**:
   - Summarize the management's insights and future outlook based on the provided MDA data.

4. **Sentiment Analysis**:
   - Summarize public sentiment based on recent news articles.
   - Include insights into any significant events or announcements that could influence market perception.

5. **Industry Analysis**:
   - Outline current industry trends and major players based on the available industry data.
   - Identify any regulatory or technological factors affecting the industry.

6. **Macroeconomic Factors**:
   - Analyze relevant macroeconomic indicators that may impact the company's performance.

7. **Risks and Opportunities**:
   - Identify key risks facing the company, including market, operational, and regulatory risks.
   - Highlight potential growth opportunities and strategic initiatives.

8. **Investment Recommendation**:
   - Based on your analysis, provide a clear recommendation (buy, hold, or sell) for the stock.
   - Support your recommendation with a well-reasoned rationale, considering both positives and negatives.

Please write in a professional tone, using precise language and relevant financial terminology. Respond in Chinese.`;

  const userPrompt = `Ticker: ${ticker}\n
1. **Sentiment Analysis**:\n${sentimentAnalysis}\n
2. **Industry Analysis**:\n${industryAnalysis}\n
3. **Financial Data**:\nCurrent Price: ${currentPrice}\nHistorical Data: ${historical}\nBalance Sheet: ${balanceSheet}\nFinancial Data: ${financial}\n
4. **Management Discussion and Analysis (MDA)**:\n${mda}\n
5. **Main Business**:\n${mainBusiness}\n\n
Based on the provided data and analyses, please provide a comprehensive investment analysis and recommendation for ${ticker}. Ensure the report length is controlled between 1500 and 3000 words, ensuring clarity and conciseness in your arguments.`;

  const messages = [
    { role: 'system', content: systemPrompt },
    {
      role: 'user',
      content: userPrompt,
    },
  ];

  return await analyzeWithOpenAI(messages);
}
  • provideFinalAnalysis:精心设计提示信息,包含所有收集的数据。
  • 保持提示信息完整性:按照要求,不破坏原有的提示信息部分。

测试应用程序

定义路由处理程序

routes/report.js 中添加路由处理程序:

router.post('/generate-report', async (req, res) => {
  const { ticker } = req.body;

  if (!ticker) {
    return res.status(400).json({ error: 'Ticker symbol is required.' });
  }

  try {
    const dataJson = readLocalJson('stock.json');
    const stockInfo = dataJson.find((item) => item.symbol === ticker);

    if (!stockInfo) {
      return res.status(404).json({ error: 'Stock information not found.' });
    }

    const { industry } = stockInfo;

    // 获取股票数据
    const stockData = await fetchStockData(ticker);

    // 执行分析
    const [sentimentAnalysis, industryAnalysis] = await Promise.all([
      performSentimentAnalysis(ticker),
      analyzeIndustry(industry || 'the industry'),
    ]);

    // 生成最终报告
    const finalAnalysis = await provideFinalAnalysis(ticker, {
      sentimentAnalysis,
      industryAnalysis,
      ...stockData,
    });

    res.json({ report: finalAnalysis });
  } catch (error) {
    console.error('Error generating report:', error.message);
    res.status(500).json({ error: 'Failed to generate report.' });
  }
});

module.exports = router;
  • 输入验证:检查是否提供了股票代码。
  • 数据收集:并发地获取股票数据和执行分析。
  • 错误处理:记录错误并在失败时发送 500 响应。

启动服务器

确保您的 app.jsroutes/report.js 已正确设置,然后启动服务器:

node app.js

发送测试请求

使用 curl 或 Postman 发送 POST 请求:

curl -X POST http://localhost:3000/api/generate-report \
  -H 'Content-Type: application/json' \
  -d '{"ticker": "AAPL"}'
  • 响应:服务器应返回包含生成报告的 JSON 对象。

总结

我们构建了一个高质量的股票报告生成器,具备以下功能:

  • 从外部 API 获取和处理股票数据
  • 使用 OpenAI API 执行高级分析
  • 生成全面的投资报告,并确保提示信息部分的完整性。

在开发过程中,我们专注于编写专业、可维护的代码,并提供了详细的解释和注释。

实施的最佳实践

  • 模块化代码结构:函数模块化,增强可重用性和清晰度。
  • 异步操作:使用 async/awaitPromise.all 实现高效的异步编程。
  • 错误处理:全面的 try-catch 块和错误消息。
  • API 抽象:将 API 交互逻辑分离,便于维护。
  • 提示工程:精心设计 OpenAI API 的提示信息,获得期望的输出。
  • 输入验证:检查必要的输入参数,防止不必要的错误。
  • 代码文档:添加 JSDoc 注释,便于理解和维护。

下一步

  • 缓存:实现缓存机制,减少重复的 API 调用。
  • 身份验证:使用身份验证和速率限制来保护 API 端点。
  • 前端开发:为用户构建前端界面,与应用程序交互。
  • 其他分析:加入技术分析或其他金融模型。

参考资料


免责声明:本应用程序仅用于教育目的。确保遵守所有 API 服务条款,并妥善处理敏感数据。