基于OpenClaw框架构建AI选股系统:从量化策略到自动化部署

基于OpenClaw框架构建AI选股系统:从量化策略到自动化部署

1. 从零到一:为什么你需要一个AI选股系统?

如果你和我一样,每天开盘前都要花大量时间翻看财经新闻、浏览研报、盯着K线图,试图从海量信息中找出那么一两个“潜力股”,那你一定深有体会:这活儿太累了,而且效率极低。人的精力是有限的,面对数千只股票和瞬息万变的市场,单靠人力筛选,不仅容易错过机会,更可能因为情绪波动做出非理性的判断。这就是我决定动手搭建一个AI选股系统的初衷——不是要取代人类的决策,而是让机器成为我们最不知疲倦、最客观冷静的“研究助理”。

这个系统的核心目标很明确:自动化、客观化、高效率。它能在每天收盘后,自动抓取最新的市场数据,运用预设的量化策略进行分析,最终筛选出符合特定条件的股票,并推送到我的手机上。整个过程,从数据获取到结果推送,完全无需人工干预。我给它起了个名字,叫“OpenClaw每日潜力股筛选器”。OpenClaw是一个新兴的、基于C#开发的AI Agent开发框架,它最大的特点就是轻量、灵活,特别适合我们这种想要快速实现自动化工作流的个人开发者。相比于一些庞大的量化平台,它更像一把瑞士军刀,让你能用最熟悉的代码(C#)和相对简单的配置,构建出功能强大的智能体(Agent)。

那么,这个系统具体能做什么?简单来说,它每天会帮我完成三件事:第一,自动获取全市场股票的实时行情和基本面数据;第二,运行我精心设计(并可以不断优化)的选股策略模型;第三,将筛选出的Top 5股票列表,通过飞书机器人发送给我,附上简单的入选理由。这样一来,我第二天开盘前只需要花10分钟,就能快速掌握经过初筛的标的,把宝贵的时间和精力集中在更深度的研究和最终的交易决策上。无论你是编程新手,还是有一定经验的开发者,只要跟着下面的步骤,都能在短时间内拥有属于自己的“AI投研助手”。

2. 核心组件拆解:OpenClaw Agent框架与选股逻辑设计

在动手敲代码之前,我们必须先搞清楚整个系统的骨架。一个能稳定运行的AI选股系统,绝不是简单的脚本堆砌,它需要几个核心组件协同工作。我的设计主要围绕OpenClaw框架展开,因为它完美契合了“轻量集成”和“灵活调度”的需求。

2.1 OpenClaw框架:你的自动化大脑

OpenClaw本质上是一个用于构建和运行AI智能体(Agent)的框架。你可以把它理解为一个“智能体操作系统”。在这个系统里,一个智能体就是一个能感知环境、进行决策并执行动作的独立程序单元。对于我们的选股系统,我们需要设计一个主智能体,它的核心职责是协调调度

这个主智能体内部,又包含几个关键的“技能”(Skill):

  1. 数据抓取技能:负责调用外部API,获取股票数据。这部分需要与行情数据源对接。
  2. 策略分析技能:这是选股的核心。它接收原始数据,运行我们设定的量化筛选规则,计算出每只股票的“得分”或“信号”。
  3. 结果推送技能:将筛选结果格式化,并通过消息通道(如飞书、钉钉、微信)发送出去。

OpenClaw的好处在于,它用清晰的C#类和方法封装了这些概念。定义一个技能,就是继承一个基类并实现几个关键方法;组装智能体,就是将这些技能像乐高积木一样组合起来。框架会负责智能体的生命周期管理、技能间的消息传递以及异常处理。这意味着我们不需要从零开始写多线程、写事件循环,可以专注于业务逻辑本身。

2.2 选股策略:量化模型的灵魂

策略部分是整个系统的灵魂。我采用的是一种多因子综合评分模型,它不追求预测股价,而是通过多个维度对股票进行“体检”,找出当前状态下相对健康、有潜力的标的。我的初版策略主要包含以下几个因子,你可以根据自身风险偏好和市场理解进行调整:

  • 动量因子:过去20个交易日的涨幅。寻找处于上升趋势的股票,但避免涨幅过大的“高位股”。我设置的条件是涨幅在5%到30%之间。
  • 波动率因子:过去20个交易日的历史波动率。过高的波动率意味着风险大,我倾向于选择波动率处于市场中等偏低水平的股票,以控制回撤。
  • 估值因子:市盈率(PE)和市净率(PB)。寻找估值相对合理的股票,避免陷入“价值陷阱”或“估值泡沫”。我会设定PE和PB的历史分位数阈值,比如要求其处于历史30%到70%的分位区间。
  • 流动性因子:日均成交额。确保股票有足够的流动性,方便进出。通常要求日均成交额大于1亿元。
  • 技术形态因子(简易版):例如,当前价格是否站在20日均线之上。这是一个简单的趋势过滤条件。

每个因子都会根据规则计算出一个0到1的分数,然后根据我赋予的权重进行加权求和,得到每只股票的综合得分。最后,对所有股票按得分从高到低排序,取前5名。这里的关键在于,权重不是一成不变的。我会根据市场风格(是成长股行情还是价值股行情)定期回测和调整权重。OpenClaw的灵活性允许我将策略参数(如权重、阈值)放在配置文件中,修改后无需重新编译代码,重启智能体即可生效。

2.3 数据源与工具链:系统的血液与骨骼

没有数据,再好的策略也是无米之炊。对于个人开发者,有几种性价比高的数据获取方式:

  • 免费/低成本API:如AKShare(一个基于Python的财经数据接口库,但我们可以通过封装HTTP服务或直接调用其数据文件来与C#交互)、Tushare(需注册获取Token)、新浪财经/东方财富的公开接口。这些接口通常有频率和数量的限制,但对于日级别的选股足够用。
  • 本地量化平台:如果你使用JoinQuant、RiceQuant等平台的本地研究环境,可以直接使用它们提供的内置数据API,数据质量更高更稳定。

我的选择是使用AKShare,因为它免费、全面且维护活跃。我写了一个简单的Python Flask服务,专门负责按需调用AKShare获取A股列表、日线行情、基本面数据等,然后我的C# OpenClaw智能体通过HTTP请求来调用这个服务。这样就将数据获取的复杂性隔离在了Python端,C#端只需处理HTTP通信和JSON解析,架构更清晰。

工具链方面,除了OpenClaw框架本身,我们还需要:

  • 开发环境:Visual Studio 2022或VS Code,安装.NET 6+ SDK。
  • 包管理器:NuGet。我们需要通过它安装OpenClaw的核心库、HTTP客户端库(如RestSharpHttpClient)、JSON处理库(如Newtonsoft.JsonSystem.Text.Json)。
  • 部署环境:可以是Windows服务器、Linux服务器(Ubuntu),或者更简单的——Docker容器。我强烈推荐使用Docker,它能将应用及其所有依赖打包,确保在任何环境下的运行一致性,彻底解决“在我机器上好好的”这类问题。

3. 实战部署:十分钟搭建OpenClaw选股智能体

理论讲完,我们进入最激动人心的实操环节。我将以在Ubuntu服务器上使用Docker部署为例,展示最清晰、最易复现的路径。如果你用Windows,步骤大同小异,主要是安装Docker Desktop和修改一下路径。

3.1 环境准备与OpenClaw安装

首先,确保你有一台云服务器(如腾讯云、阿里云的轻量应用服务器)或本地Linux机器,并已安装Docker和Docker Compose。

  1. 获取OpenClaw Docker镜像:OpenClaw社区提供了官方镜像,大大简化了部署。打开终端,执行以下命令拉取镜像:

    docker pull crestodian/openclaw:latest

    这里你可能注意到了crestodian这个发布者。根据社区资料,Crestodian是OpenClaw项目相关的贡献者或组织,其发布的镜像通常是经过测试的相对稳定版本。

  2. 准备配置文件目录:在服务器上创建一个项目目录,比如/home/ubuntu/ai_stock_picker,并在里面创建两个子目录:configskills

    mkdir -p /home/ubuntu/ai_stock_picker/{config,skills,logs}

    config用于存放智能体和技能的配置文件,skills用于存放我们自定义的技能代码(编译后的DLL文件),logs用于存放运行日志。

  3. 编写智能体配置文件:在config目录下创建StockPickerAgent.json。这个文件定义了我们的智能体。

    { "AgentName": "每日选股智能体", "Description": "自动执行选股策略并推送结果", "Skills": [ { "SkillName": "DataFetcherSkill", "AssemblyPath": "/app/skills/StockPickerSkills.dll", "ClassName": "StockPickerSkills.DataFetcherSkill" }, { "SkillName": "StrategyAnalyzerSkill", "AssemblyPath": "/app/skills/StockPickerSkills.dll", "ClassName": "StockPickerSkills.StrategyAnalyzerSkill" }, { "SkillName": "NotifierSkill", "AssemblyPath": "/app/skills/StockPickerSkills.dll", "ClassName": "StockPickerSkills.NotifierSkill" } ], "Schedule": "0 30 16 * * 1-5", "MaxConcurrentOperations": 1 }
    • Skills数组列出了智能体拥有的三个技能,它们都位于同一个DLL文件中。
    • Schedule是Cron表达式,这里表示“每周一到周五的下午16:30(收盘后)执行”。这是关键,确保在交易日收盘后运行。
    • MaxConcurrentOperations设为1,保证同一时间只执行一个任务,避免资源冲突。

3.2 开发核心技能:数据、策略与通知

接下来,我们需要在本地开发环境(Windows/Mac)上用Visual Studio创建C#类库项目,编写这三个技能。项目需要引用OpenClaw的NuGet包(如OpenClaw.Core)。

1. DataFetcherSkill(数据抓取技能)这个技能负责调用我们之前提到的Python数据服务。核心是重写ExecuteAsync方法。

using OpenClaw.Core; using System.Net.Http; using System.Threading.Tasks; namespace StockPickerSkills { public class DataFetcherSkill : SkillBase { private readonly HttpClient _httpClient; private readonly string _dataServiceUrl; public DataFetcherSkill(IConfiguration configuration) { // 从配置读取数据服务地址 _dataServiceUrl = configuration["DataService:BaseUrl"]; _httpClient = new HttpClient(); } public override async Task<SkillResult> ExecuteAsync(SkillContext context) { LogInfo("开始获取A股市场数据..."); // 示例:获取全市场股票列表和昨日行情 var stockListResponse = await _httpClient.GetAsync($"{_dataServiceUrl}/api/stock_basic"); if (!stockListResponse.IsSuccessStatusCode) { return SkillResult.Failure($"获取股票列表失败: {stockListResponse.StatusCode}"); } var stockList = await stockListResponse.Content.ReadAsStringAsync(); // 这里可以继续获取市盈率、市净率等基本面数据 // var peResponse = await _httpClient.GetAsync($"{_dataServiceUrl}/api/stock_valuation"); // 将获取到的原始数据存入上下文,供下一个技能使用 context.SetData("RawStockList", stockList); // context.SetData("RawValuationData", peData); LogInfo("市场数据获取完成。"); return SkillResult.Success("数据抓取成功"); } } }

注意:在实际项目中,你需要处理JSON反序列化,将字符串转换为强类型的股票对象列表。同时,要考虑网络异常、数据缺失的重试机制。我的做法是引入Polly库来实现简单的重试策略。

2. StrategyAnalyzerSkill(策略分析技能)这是最核心的部分,接收原始数据,运行量化模型。

using OpenClaw.Core; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; namespace StockPickerSkills { public class StrategyAnalyzerSkill : SkillBase { public override async Task<SkillResult> ExecuteAsync(SkillContext context) { LogInfo("开始执行选股策略分析..."); // 从上下文中取出上一个技能存储的数据 var rawStockListJson = context.GetData<string>("RawStockList"); if (string.IsNullOrEmpty(rawStockListJson)) { return SkillResult.Failure("未获取到有效的股票列表数据。"); } // 解析JSON数据(这里简化处理,实际应定义实体类) var stockArray = JArray.Parse(rawStockListJson); var scoredStocks = new List<StockScore>(); foreach (var stock in stockArray) { var code = stock["code"].ToString(); var name = stock["name"].ToString(); var closePrice = stock["close"].ToObject<double>(); var pe = stock["pe"].ToObject<double?>() ?? 0; // 处理空值 var pb = stock["pb"].ToObject<double?>() ?? 0; var turnover = stock["turnover"].ToObject<double>(); // 成交额,单位:万 // 计算因子分数(示例,需根据完整数据实现) double momentumScore = CalculateMomentumScore(stock); double volatilityScore = CalculateVolatilityScore(stock); double valuationScore = CalculateValuationScore(pe, pb); double liquidityScore = (turnover > 10000) ? 1.0 : 0.0; // 成交额大于1亿得满分 // 加权综合得分(权重可配置) double totalScore = momentumScore * 0.3 + volatilityScore * 0.2 + valuationScore * 0.3 + liquidityScore * 0.2; scoredStocks.Add(new StockScore { Code = code, Name = name, Score = totalScore }); } // 按得分降序排序,取前5 var top5Stocks = scoredStocks.OrderByDescending(s => s.Score).Take(5).ToList(); // 将结果存入上下文 context.SetData("Top5Stocks", top5Stocks); LogInfo($"策略分析完成,筛选出{top5Stocks.Count}支潜力股。"); return SkillResult.Success("策略分析完成"); } // 以下是简化的因子计算函数,实际需要更复杂的历史数据计算 private double CalculateMomentumScore(JToken stock) { /* 实现20日涨幅计算逻辑 */ } private double CalculateVolatilityScore(JToken stock) { /* 实现20日波动率计算逻辑 */ } private double CalculateValuationScore(double pe, double pb) { /* 实现PE/PB分位数计算逻辑 */ } } public class StockScore { public string Code { get; set; } public string Name { get; set; } public double Score { get; set; } } }

踩坑提示:因子计算依赖历史数据。如果你的数据服务只提供最新日期的数据,就需要在DataFetcherSkill中一次性获取足够长时间序列的数据(比如最近60个交易日),并传递给分析技能。数据在技能间的传递要规划好,避免频繁的IO操作拖慢速度。

3. NotifierSkill(通知技能)将结果推送到飞书。

using OpenClaw.Core; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace StockPickerSkills { public class NotifierSkill : SkillBase { private readonly HttpClient _httpClient; private readonly string _feishuWebhookUrl; public NotifierSkill(IConfiguration configuration) { _feishuWebhookUrl = configuration["Feishu:WebhookUrl"]; _httpClient = new HttpClient(); } public override async Task<SkillResult> ExecuteAsync(SkillContext context) { var top5Stocks = context.GetData<List<StockScore>>("Top5Stocks"); if (top5Stocks == null || top5Stocks.Count == 0) { LogWarning("未获取到选股结果,跳过通知。"); return SkillResult.Success("无结果,通知跳过"); } // 构建飞书富文本消息 var messageContent = new { msg_type = "post", content = new { post = new { zh_cn = new { title = "📈 今日AI选股结果", content = new List<object[]> { new object[] { new { tag = "text", text = "根据量化策略筛选,今日潜力股Top 5如下:\n" } }, new object[] { new { tag = "text", text = "----------------------------\n" } } } } } } }; // 动态添加股票信息 foreach (var stock in top5Stocks) { var stockText = $"**{stock.Name} ({stock.Code})** - 综合评分: {stock.Score:F2}\n"; ((JArray)messageContent.content.post.zh_cn.content).Add(new JArray(new { tag = "text", text = stockText })); } // 添加尾部说明 ((JArray)messageContent.content.post.zh_cn.content).Add(new JArray(new { tag = "text", text = "\n----------------------------\n*数据来源: AKShare,策略模型: 多因子综合评分*" })); var jsonPayload = Newtonsoft.Json.JsonConvert.SerializeObject(messageContent); var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(_feishuWebhookUrl, httpContent); if (response.IsSuccessStatusCode) { LogInfo("选股结果已成功推送至飞书。"); return SkillResult.Success("通知发送成功"); } else { var error = await response.Content.ReadAsStringAsync(); LogError($"飞书消息发送失败: {error}"); return SkillResult.Failure($"通知发送失败: {response.StatusCode}"); } } } }

开发完成后,将项目编译成StockPickerSkills.dll,并上传到服务器的/home/ubuntu/ai_stock_picker/skills/目录下。

3.3 配置与启动:让智能体跑起来

  1. 编写Docker Compose文件:在项目根目录创建docker-compose.yml,这是最优雅的部署方式。

    version: '3.8' services: openclaw-stock-picker: image: crestodian/openclaw:latest container_name: ai_stock_picker restart: unless-stopped # 自动重启,保证服务持续运行 volumes: - ./config:/app/config # 挂载配置文件目录 - ./skills:/app/skills # 挂载技能DLL目录 - ./logs:/app/logs # 挂载日志目录 environment: - ASPNETCORE_ENVIRONMENT=Production - OpenClaw__AgentConfigPath=/app/config/StockPickerAgent.json # 如果需要连接宿主机的服务(如Python数据服务),可能需要配置network_mode: host # network_mode: host

    关键点在于volumes,它将宿主机的目录映射到容器内部,这样我们修改配置或更新技能DLL时,只需替换宿主机的文件,然后重启容器即可,无需重新构建镜像。

  2. 配置应用设置:在config目录下创建appsettings.json,存放数据服务地址、飞书Webhook等敏感或可配置信息。

    { "DataService": { "BaseUrl": "http://你的服务器IP:5000" // Python数据服务的地址 }, "Feishu": { "WebhookUrl": "https://open.feishu.cn/open-apis/bot/v2/hook/你的Webhook令牌" } }

    安全提醒:切勿将包含真实Token或密钥的配置文件提交到Git等版本控制系统。可以通过Docker Compose的environment部分注入环境变量,或在生产环境使用密钥管理服务。

  3. 启动服务:在项目根目录执行一条命令,一切就绪。

    docker-compose up -d

    使用docker-compose logs -f openclaw-stock-picker可以实时查看智能体的启动和运行日志。如果一切正常,你会看到智能体加载了配置文件、注册了技能,并等待Cron调度触发。

4. 调优、排错与进阶思考

系统跑起来只是第一步,要让其真正可靠、有用,还需要持续的观察、调优和问题排查。

4.1 日常监控与日志分析

OpenClaw框架和我们的技能代码都会产生日志。日志是我们排查问题的第一手资料。我强烈建议将日志级别设置为InformationDebug,并定期检查logs目录下的文件。

  • 查看定时任务是否触发:在设定的时间(交易日16:30)后,查看日志中是否有“开始获取A股市场数据…”等字样。如果没有,检查服务器的系统时间、时区设置,以及Docker容器的时钟是否与宿主机同步。
  • 检查技能执行链路:观察三个技能是否按顺序执行成功。如果某个技能失败,日志会明确报错,例如网络超时、数据解析异常等。
  • 验证飞书消息:确保消息能成功送达。如果没收到,检查飞书机器人的Webhook URL是否正确,以及消息内容格式是否符合飞书的要求。

一个常见的坑是技能执行超时。如果数据获取或策略分析非常耗时,可能会超过OpenClaw框架默认的任务超时时间。这时需要在智能体配置文件中增加"TimeoutInSeconds": 300这样的配置,为任务预留充足时间。

4.2 策略回测与迭代优化

初始的策略参数和权重很可能不是最优的。我们需要一个反馈循环来优化它。

  1. 历史回测:编写一个简单的回测脚本(可以用Python,也可以用C#),使用历史数据运行你的选股策略,看看如果按照这个策略在过去一段时间内交易,收益率、夏普比率、最大回撤等指标如何。这是验证策略有效性的基础。
  2. 模拟盘跟踪:将每日选出的5支股票放入一个模拟股票池,虚拟买入并跟踪其后续一段时间的表现(如5日、20日涨跌幅)。记录每只股票的“命中率”(上涨概率)和平均收益。
  3. 分析归因:定期(比如每季度)分析策略的表现。是哪类因子贡献了主要收益?在哪种市场环境下(牛市、熊市、震荡市)策略会失效?根据分析结果,动态调整因子权重或引入新的过滤条件。

我的经验是,不要追求一个“圣杯”策略。市场在变,有效的因子也在变。这个系统的价值在于提供了一个可以快速迭代、测试想法的自动化平台。你可以轻松地创建StrategyAnalyzerSkillV2V3,通过修改配置文件来切换使用哪个技能,从而对比不同策略的效果。

4.3 常见问题与故障排除

  • OpenClaw启动失败,提示“无法加载技能DLL”

    • 检查:技能DLL的路径在配置文件中是否正确?是否上传到了正确的挂载目录?
    • 检查:技能DLL的依赖项(如Newtonsoft.Json)是否齐全?可以尝试将技能项目发布为“独立部署”模式,将所有依赖打包进一个文件夹,然后上传整个文件夹。
    • 检查:Docker容器内的.NET运行时版本是否与编译技能DLL时使用的目标框架兼容?确保使用匹配的镜像标签。
  • 数据获取技能总是超时或返回空数据

    • 检查:Python数据服务是否正常运行?在宿主机上用curl http://localhost:5000/api/stock_basic测试一下。
    • 检查:如果数据服务在宿主机,Docker容器默认的桥接网络可能无法通过localhost访问。需要改用宿主机的真实IP,或将Docker Compose的网络模式改为host(注意安全风险)。
    • 检查:AKShare等数据源接口是否有更新?免费接口不稳定是常态,需要有备用方案或错误重试机制。
  • 飞书消息格式错误,发送失败

    • 检查:飞书机器人消息格式非常严格。使用在线JSON格式化验证工具检查你构建的消息体。特别注意富文本内容中content字段的嵌套结构。
    • 检查:飞书机器人是否已被添加到正确的群聊?Webhook URL是否还有效?

4.4 系统扩展与进阶玩法

当基础系统稳定运行后,你可以考虑以下扩展方向,让它变得更强大:

  1. 多策略并行与投票机制:不再是一个策略定乾坤。可以部署多个不同的StrategyAnalyzerSkill(例如价值策略、成长策略、技术策略),让它们同时对市场进行扫描。最后用一个“投票聚合技能”来综合各策略的推荐结果,形成最终的Top 5名单。这能有效分散单一策略失效的风险。
  2. 集成大语言模型(LLM)进行解读:单纯的股票列表和分数可能不够直观。可以在NotifierSkill之后,增加一个LLMCommentSkill。将选股结果和简单的财务指标、近期新闻摘要一起,发送给本地部署的Ollama(运行Llama 3、Qwen等开源模型),让AI生成一段简要的“今日选股逻辑解读”或“风险提示”,附在飞书消息里。这能让你的报告更具可读性和洞察力。
  3. 接入更多数据源:除了行情和基本面,可以接入舆情数据(如股吧热词)、产业链数据、宏观经济指标等,作为新的因子纳入模型,让选股维度更立体。
  4. 实现简单的自动化交易(谨慎!):这是一个更进阶、风险也更高的领域。你可以让系统在筛选出股票后,通过券商提供的条件单API或自动化交易接口,执行预设的买入指令。但必须极度谨慎,务必加入严格的风控规则(如单只股票最大仓位、总体仓位控制、止损条件),并先在模拟环境中充分测试。我强烈建议将交易决策权留给人,系统只做辅助推荐。

搭建这个系统的过程,与其说是为了得到一个“黑箱”赚钱工具,不如说是一次绝佳的、将软件开发、数据工程和金融知识结合的实践。它强迫你去思考市场的逻辑,去理解数据背后的意义,去设计稳健的系统架构。每天下午准时收到的选股清单,更像是对自己逻辑和代码的一次次小考。经过几个月的运行和迭代,我不仅对市场有了更量化的认识,C#和Docker的熟练度也大大提升。最重要的是,它把我从重复、繁琐的初筛劳动中解放了出来,让我能更专注于策略的思考和深度的公司研究。