用前端技术薅羊毛:我做了一款 LOF 基金套利工具(附核心思路)

751 阅读6分钟

LOF基金套利工具:实时数据获取与溢价计算的技术实现

截屏2025-12-28 10.11.41.png

需求背景

LOF(Listed Open-Ended Fund)基金兼具开放式和封闭式基金的特点,既可以在一级市场申购赎回,也可以在二级市场买卖交易。由于两个市场的定价机制不同,经常会出现价格差异,这就产生了套利机会。

开发一个LOF基金套利工具的核心需求包括:

  1. 实时获取基金的场内交易价格
  2. 获取基金的场外净值(官方净值和实时估值)
  3. 自动计算溢价率,识别套利机会
  4. 提供直观的界面展示和数据分析

本文将详细介绍如何实现这样一个工具,重点解析基金数据获取和溢价计算的核心技术。

技术选型

前端框架与工具

  • React 19:用于构建用户界面,提供组件化开发能力
  • TypeScript:增强代码类型安全性和可维护性
  • Vite:现代化的前端构建工具,提供快速的开发体验
  • Tailwind CSS:原子化CSS框架,简化样式开发

数据获取方案

  • JSONP:由于需要跨域获取第三方金融数据,JSONP是一种可靠的解决方案
  • 第三方数据源
    • 腾讯财经(qt.gtimg.cn):提供实时场内交易价格
    • 天天基金网(fundgz.1234567.com.cn):提供基金实时估值和官方净值

状态管理

  • React Hooks:使用useState、useEffect、useMemo等Hook进行状态管理和性能优化

核心代码解析

1. 数据类型定义

首先定义核心数据结构,确保类型安全:

// src/types.ts
export interface FundData {
  id: string;
  code: string;
  name: string;
  marketPrice: number; // 场内价格
  estimatedNav: number; // 实时估值 (IOPV, gsz)
  t1ActualNav: number; // T-1 场外净值 (官方昨日净值)
  t2ActualNav: number; // T-2 场外净值(若有)
  prevClose: number;
  purchaseLimit: string; // 限购金额或状态
  lastUpdate: string;
}

export interface ArbitrageRow extends FundData {
  premiumRate: number; // 溢价率 % (基于estimatedNav)
}

2. 跨域数据获取(JSONP实现)

由于需要从第三方金融网站获取数据,跨域是不可避免的问题。我们使用JSONP来解决这个问题:

// src/services/marketService.ts
// JSONP脚本加载助手
const loadScript = (url: string, charset: string = 'gbk'): Promise<void> => {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.charset = charset;
    script.async = true;
    script.onload = () => {
      // 加载完成后清理脚本标签
      if (document.body.contains(script)) {
        document.body.removeChild(script);
      }
      resolve();
    };
    script.onerror = () => {
      if (document.body.contains(script)) {
        document.body.removeChild(script);
      }
      console.warn(`Script load failed for ${url}`);
      // 即使失败也resolve,避免阻塞其他promise
      resolve(); 
    };
    document.body.appendChild(script);
  });
};

3. 实时数据获取逻辑

核心函数fetchFundRealTimeData负责获取基金的实时交易数据和净值信息:

// src/services/marketService.ts
export const fetchFundRealTimeData = async (currentFunds: FundData[]): Promise<FundData[]> => {
  // 1. 准备市场代码(深圳16xxxx前缀sz,上海50xxxx前缀sh)
  const marketCodes = currentFunds.map(f => {
    const prefix = f.code.startsWith('5') ? 'sh' : 'sz';
    return `${prefix}${f.code}`;
  }).join(',');

  // 2. 从腾讯财经获取实时价格
  try {
    const timestamp = Date.now();
    await loadScript(`https://qt.gtimg.cn/q=${marketCodes}&t=${timestamp}`, 'gbk');
  } catch (e) {
    console.error("Failed to fetch prices from Tencent", e);
  }

  // 3. 从天天基金网获取实时估值
  const navMap: Record<string, { gsz: number; dwjz: number; gztime?: string; jzrq?: string; name?: string }> = {};

  // 定义天天基金网NAV的全局回调
  (window as any).jsonpgz = (data: any) => {
    if (data && data.fundcode) {
      navMap[data.fundcode] = {
        gsz: parseFloat(data.gsz),   // 实时估值 (IOPV)
        dwjz: parseFloat(data.dwjz), // 实际净值 (官方净值)
        gztime: data.gztime,
        jzrq: data.jzrq,
        name: data.name
      };
    }
  };

  // 并行获取NAV
  const navPromises = currentFunds.map(fund =>
    loadScript(`https://fundgz.1234567.com.cn/js/${fund.code}.js?rt=${Date.now()}`)
  );

  await Promise.all(navPromises);

  // 4. 合并数据
  return currentFunds.map(fund => {
    // 处理腾讯财经数据...
    // 处理天天基金网数据...
    // 合并并返回结果...
  });
};

4. 溢价计算逻辑

溢价率是LOF基金套利的核心指标,计算公式为:溢价率 = (场内价格 - 净值) / 净值 * 100%

// src/services/marketService.ts
export const calculatePremium = (marketPrice: number, nav: number): number => {
  if (nav === 0) return 0;
  if (Math.abs(nav) < 0.0001) return 0;
  return ((marketPrice - nav) / nav) * 100;
};

在主应用中,我们实现了智能的净值选择逻辑:

// src/App.tsx
// 计算溢价率的行数据
const tableData: ArbitrageRow[] = useMemo(() => {
  return funds.map(fund => {
    // 逻辑:优先使用T-1场外净值计算溢价;若无T-1,则使用T-2净值,再回退到当日估算净值
    const calculationNav = fund.t1ActualNav > 0 ? fund.t1ActualNav : (fund.t2ActualNav > 0 ? fund.t2ActualNav : fund.estimatedNav);

    return {
      ...fund,
      premiumRate: calculatePremium(fund.marketPrice, calculationNav)
    };
  }).sort((a, b) => {
    // 按溢价率绝对值排序,突出最大套利机会
    return Math.abs(b.premiumRate) - Math.abs(a.premiumRate);
  }); 
}, [funds, sortOrder]);

5. 实时数据更新机制

使用React的useEffect Hook实现数据的定时更新:

// src/App.tsx
// 实时数据更新逻辑
useEffect(() => {
  let intervalId: NodeJS.Timeout | null = null;

  const updateFundData = async () => {
    try {
      setIsRefreshing(true);
      // 获取实时交易数据和估值
      const updatedFunds = await fetchFundRealTimeData(funds);
      
      // 获取基金详情(如限购信息)
      const fundDetails = await fetchFundDetails(updatedFunds);
      
      // 更新基金数据
      const finalFunds = updatedFunds.map(fund => ({
        ...fund,
        purchaseLimit: fundDetails[fund.code]?.limit || fund.purchaseLimit
      }));
      
      setFunds(finalFunds);
    } catch (error) {
      console.error("Failed to update fund data:", error);
    } finally {
      setIsRefreshing(false);
    }
  };

  // 初始加载数据
  updateFundData();

  // 如果开启实时更新,设置定时器
  if (isLive) {
    intervalId = setInterval(updateFundData, 15000); // 每15秒更新一次
  }

  // 清理函数
  return () => {
    if (intervalId) {
      clearInterval(intervalId);
    }
  };
}, [isLive, funds]);

踩坑点与解决方案

1. 跨域数据获取问题

问题:直接从浏览器请求第三方金融网站API会遇到跨域限制。

解决方案:使用JSONP技术获取数据,通过动态创建script标签并指定回调函数来绕过跨域限制。

// JSONP脚本加载实现
const loadScript = (url: string, charset: string = 'gbk'): Promise<void> => {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.charset = charset;
    script.async = true;
    // ... 事件处理和清理逻辑
    document.body.appendChild(script);
  });
};

2. 数据源可靠性与备份

问题:单一数据源可能会出现不稳定或数据缺失的情况。

解决方案

  • 同时使用多个数据源(腾讯财经和天天基金网)
  • 实现数据优先级选择逻辑,当一个数据源不可用时自动切换到另一个
  • 对异常数据进行过滤和验证
// 净值数据优先级选择
let estimatedNav = 0;
if (tencentEstimatedNav > 0) {
  estimatedNav = tencentEstimatedNav;
} else if (navData && !isNaN(navData.gsz) && navData.gsz > 0) {
  estimatedNav = navData.gsz;
} else {
  estimatedNav = fund.estimatedNav;
}

3. 数据格式处理与转换

问题:不同数据源返回的数据格式不一致,需要进行统一处理。

解决方案

  • 定义统一的数据模型(如FundData接口)
  • 在数据获取后进行格式转换和字段映射
  • 对数值型数据进行严格的类型检查和转换
// 数值转换与验证
const gsz = parseFloat(data.gsz);   // 实时估值 (IOPV)
const dwjz = parseFloat(data.dwjz); // 实际净值 (官方净值)

// 日期处理
const gz = navData.gztime ? new Date(navData.gztime) : null;
const jz = navData.jzrq ? new Date(navData.jzrq) : null;
if (gz && jz && !isNaN(gz.getTime()) && !isNaN(jz.getTime())) {
  const dayDiff = Math.round((gz.getTime() - jz.getTime()) / (1000 * 60 * 60 * 24));
  // 根据日期差分配T-1/T-2净值
}

4. 性能优化问题

问题:频繁的数据更新可能导致页面性能下降。

解决方案

  • 使用React.memo和useMemo减少不必要的重新渲染
  • 批量更新数据,避免频繁的状态更新
  • 合理设置数据更新间隔(15-30秒)
// 使用useMemo缓存计算结果
const tableData: ArbitrageRow[] = useMemo(() => {
  return funds.map(fund => {
    // 计算逻辑
  }).sort(/* 排序逻辑 */);
}, [funds, sortOrder]);

总结

本文详细介绍了LOF基金套利工具的技术实现,重点解析了以下内容:

  1. 数据获取:使用JSONP技术跨域获取基金的实时交易价格和净值数据,实现了多数据源备份和优先级选择。

  2. 溢价计算:实现了智能的净值选择逻辑,优先使用官方净值,确保溢价率计算的准确性。

  3. 实时更新:使用React的useEffect和setInterval实现数据的定时更新,同时考虑了性能优化。

  4. 类型安全:通过TypeScript的类型定义,确保了代码的可维护性和可靠性。

这个工具不仅可以帮助投资者识别LOF基金的套利机会,也展示了如何在前端应用中处理实时金融数据、解决跨域问题以及优化性能的实践经验。

在实际应用中,还可以进一步扩展功能,如添加历史数据分析、AI辅助决策、风险评估等,使工具更加完善和实用。