1. 从冲动交易到代码纪律:一个普通投资者的觉醒
你有没有过这样的经历?盯盘时,看到股价突然拉升,肾上腺素飙升,生怕错过这班车,手指不听使唤地就点了买入。或者,持仓的股票连续阴跌,内心被恐惧和焦虑填满,最终在最低点附近割肉离场。事后复盘,捶胸顿足,明明知道不该这么操作,但情绪上头的那一刻,理智完全失效。我曾经就是这样,一个在股市里交了不少学费的普通人。直到我开始尝试用代码来“管住自己的手”,才真正体会到什么叫“交易纪律”,而这一切的起点,不是什么高深的金融工程,而是我们熟悉的Python和一些基础的AI思路。
这个项目,我称之为“AI股票小助手”,但它并非一个能预测涨跌、保证盈利的“圣杯”。它的核心目标非常朴素:用程序化的规则,替代我情绪化的决策,强制自己执行预设的交易计划。听起来很简单,但正是这种简单,让我从频繁的追涨杀跌中解脱出来,开始用一种更冷静、更系统的方式观察市场。它更像是一个“市场观察笔记”的自动化工具,帮我记录、分析,并在关键时刻“拉住”我。
整个过程涉及几个关键词:Python是实现的工具,量化交易是背后的思想框架,回测是检验想法可行性的必要步骤,而纪律,则是整个系统想要达成的最终状态。至于AI,在这里并非指代一个能自主思考的超级大脑,而是利用一些算法(比如简单的机器学习模型或规则引擎)来处理数据、识别模式,辅助做出更理性的判断,其复杂程度完全可以根据个人能力调整,从简单的条件判断到复杂的模型都可以。
无论你是编程新手,还是有一定基础的投资者,希望从手动交易的泥潭中跳出来,这个基于Python构建个人化市场观察与纪律执行系统的思路,或许能给你带来一些切实的启发。它不承诺财富,但承诺给你一个更清晰、更少情绪干扰的投资视角。
2. 系统核心设计:你的交易“宪法”与“自动执行官”
在开始敲代码之前,我们必须想清楚这个“小助手”到底要干什么。它的角色不是代替你思考,而是充当一个铁面无私的“执行官”,严格执行你提前制定好的“交易宪法”。这个设计过程,比写代码本身更重要。
2.1 定义你的“交易宪法”:从模糊感觉到清晰规则
大多数人的交易决策是模糊的:“感觉要涨了”、“跌太多了该反弹了”。而代码只认识清晰的规则。因此,第一步是将你的投资逻辑或观察逻辑,翻译成计算机能理解的规则。
1. 观察指标与数据源:你需要决定观察什么。对于普通人来说,可以从最基础、最易获取的数据开始:
- 价格与成交量:收盘价、开盘价、最高价、最低价、成交额。这是最基本的。
- 技术指标:移动平均线(MA)、相对强弱指数(RSI)、布林带(Bollinger Bands)、MACD。这些都有成熟的库(如
ta-lib或pandas计算)可以轻松计算。 - 基本面快照(可选):对于长线观察,可以定期抓取市盈率(PE)、市净率(PB)等,但数据源和更新频率需要考量。
- 数据源选择:
- 免费/开源:
akshare、yfinance(需注意网络环境)、tushare(部分数据需积分)。akshare是国内数据源,覆盖A股、港股、期货、基金等,对个人学习非常友好。 - 本地化存储:务必设计一个本地数据库(如SQLite)或文件系统来存储历史数据。每次运行都从网络实时获取全部历史数据效率低下且不稳定。我的做法是,每天收盘后运行一次脚本,将当日数据增量更新到本地库中。
- 免费/开源:
2. 制定具体的规则(信号):这是“宪法”的核心条款。规则必须是无歧义的“如果...那么...”语句。
- 买入观察信号(示例):
- 规则1:收盘价连续3日站在20日移动平均线之上,且当日成交量大于20日平均成交量的1.5倍。
- 规则2:RSI(14)从低于30的区域上穿30(超卖反弹)。
- 规则3:股价突破过去20日最高点(海龟交易法则简化版)。
- 卖出/风控规则(示例,更重要!):
- 规则A:持仓亏损达到-8%时,无条件卖出。(这是管住手、防止深套的关键!)
- 规则B:收盘价跌破10日移动平均线时,卖出。
- 规则C:盈利达到+20%时,卖出一半锁定利润。
- 规则D:RSI(14)高于70时,发出警示,考虑减仓。
注意:初始阶段,规则一定要简单、可解释。不要试图用一个包含10个条件的复杂规则去捕捉所有机会。简单的规则更容易回测和迭代。我的第一条风控规则就是“亏损-8%止损”,它帮我避免了无数次由“再等等看”的侥幸心理酿成的大亏。
3. 定义执行方式:规则触发了,然后呢?这里有两种模式:
- 纯观察模式(推荐起步):系统只负责监控,当规则触发时,通过邮件、微信推送(可用
Server酱、Pushplus等工具)、或桌面通知告诉你:“XX股票触发了买入观察信号”或“警告!XX股票触及-8%止损线!”。最终的交易操作,依然由你手动执行。这给了你一个缓冲和最终确认的机会,但系统已经完成了“发现事实”和“发出警报”的职责。 - 半自动/全自动模式(进阶):系统在触发规则后,自动生成订单,但需要你手动登录券商软件确认下单;或通过券商提供的API(如一些券商给量化客户的接口)自动执行。对于绝大多数普通人,强烈建议从纯观察模式开始。自动交易涉及资金安全、网络延迟、订单状态查询等一系列复杂问题,门槛和风险都高很多。
2.2 系统架构蓝图:模块化让一切更清晰
一个可维护的系统需要好的架构。我们的“小助手”可以分成几个相对独立的模块:
数据层 (Data Layer) ├── 数据获取模块 (Fetcher):从akshare/yfinance获取数据 ├── 数据存储模块 (Storage):将数据存入SQLite/CSV └── 数据清洗模块 (Cleaner):处理缺失值、异常值 计算层 (Engine Layer) ├── 指标计算模块 (Indicator):计算MA, RSI, MACD等 ├── 信号生成模块 (Signal):根据规则,判断生成买入/卖出信号 └── 风险监控模块 (Risk):独立监控持仓盈亏,触发风控警报 应用层 (App Layer) ├── 回测模块 (Backtest):用历史数据验证规则有效性 ├── 监控执行模块 (Monitor):定时运行,检查信号并推送提醒 └── 报表生成模块 (Report):生成每日观察摘要、信号记录这样的分层设计,使得每个部分都可以独立开发和测试。比如,你可以先专心把数据获取和存储做稳定,再开发信号计算,最后做回测和监控。代码不会乱成一锅粥。
3. 从零搭建:关键代码实现与避坑指南
接下来,我们进入实操环节。我会用最核心的代码片段,展示如何实现上述模块的关键功能。假设我们的目标是观察A股市场,使用akshare获取数据,使用pandas和numpy进行计算。
3.1 环境准备与数据基石
首先,确保你的Python环境(建议3.8以上)已经安装必要的库。
pip install akshare pandas numpy sqlalchemy schedule requests数据获取与存储模块:这是所有分析的基础,一定要稳。
# data_fetcher.py import akshare as ak import pandas as pd from sqlalchemy import create_engine import datetime import time class StockDataFetcher: def __init__(self, db_path='stock_data.db'): # 使用SQLite数据库,简单方便 self.engine = create_engine(f'sqlite:///{db_path}') def fetch_daily_data(self, stock_code, start_date='20200101'): """获取单只股票的日线数据""" # akshare的A股代码需要加市场前缀,如‘sh600000’ try: # 这里以‘日频率’数据为例,ak.stock_zh_a_hist 是常用接口,请以akshare最新文档为准 df = ak.stock_zh_a_hist(symbol=stock_code, period="daily", start_date=start_date, adjust="qfq") # 重命名列,标准化 df.rename(columns={ '日期': 'date', '开盘': 'open', '收盘': 'close', '最高': 'high', '最低': 'low', '成交量': 'volume', '成交额': 'amount', '振幅': 'amplitude', '涨跌幅': 'pct_chg', '涨跌额': 'change', '换手率': 'turnover' }, inplace=True) df['date'] = pd.to_datetime(df['date']) df.set_index('date', inplace=True) df['code'] = stock_code return df except Exception as e: print(f"获取数据失败 {stock_code}: {e}") return pd.DataFrame() def update_or_insert_data(self, df, table_name='daily_price'): """将数据更新或插入到数据库""" if df.empty: return try: # 使用pandas的to_sql,if_exists='append',但需要自己处理重复数据 # 更健壮的做法是先读取已有数据,合并去重后再写入 existing_dates = pd.read_sql_query( f"SELECT DISTINCT date FROM {table_name} WHERE code = '{df.iloc[0]['code']}'", self.engine )['date'].tolist() if not pd.read_sql_query(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'", self.engine).empty else [] df_to_insert = df[~df.index.isin(existing_dates)] if not df_to_insert.empty: df_to_insert.to_sql(table_name, self.engine, if_exists='append', index=True) print(f"插入 {len(df_to_insert)} 条新数据 for {df.iloc[0]['code']}") except Exception as e: # 如果表不存在,则创建 df.to_sql(table_name, self.engine, if_exists='fail', index=True) print(f"创建新表并插入数据 for {df.iloc[0]['code']}") # 使用示例 fetcher = StockDataFetcher() df = fetcher.fetch_daily_data('sh600000', start_date='20230101') fetcher.update_or_insert_data(df)避坑指南1:数据清洗与复权
akshare等接口返回的数据,复权因子至关重要。做长期回测,必须使用后复权数据(adjust=“hfq”)或前复权数据(adjust=“qfq”),这样才能真实反映股价长期走势。我一开始用不复权数据回测,结果完全失真。另外,注意处理成交量为0的停牌日,在计算移动平均成交量时,需要过滤或填充这些日期。
3.2 信号引擎:将规则翻译成代码
有了数据,我们就可以实现信号生成模块了。
# signal_engine.py import pandas as pd import numpy as np # 可以安装TA-Lib用于计算技术指标,或者用pandas手动算 # pip install TA-Lib (在Windows上可能需要下载预编译包) class SimpleSignalEngine: @staticmethod def calculate_indicators(df): """计算常用技术指标""" # 确保数据按日期升序排列 df = df.sort_index() # 计算移动平均线 df['MA20'] = df['close'].rolling(window=20, min_periods=1).mean() df['MA10'] = df['close'].rolling(window=10, min_periods=1).mean() # 计算RSI (简化版,生产环境建议用TA-Lib或更严谨的实现) delta = df['close'].diff() gain = (delta.where(delta > 0, 0)).rolling(window=14).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean() rs = gain / loss df['RSI14'] = 100 - (100 / (1 + rs)) # 计算20日平均成交量 df['Volume_MA20'] = df['volume'].rolling(window=20, min_periods=1).mean() return df def generate_signals(self, df): """根据规则生成信号""" df = self.calculate_indicators(df) signals = pd.DataFrame(index=df.index) signals['code'] = df['code'] # 规则1: 收盘价>MA20,且成交量放大 condition1 = (df['close'] > df['MA20']) & (df['volume'] > 1.5 * df['Volume_MA20']) # 规则2: RSI上穿30 df['RSI_prev'] = df['RSI14'].shift(1) condition2 = (df['RSI_prev'] < 30) & (df['RSI14'] >= 30) # 买入信号 (1表示触发) signals['buy_signal_rule1'] = np.where(condition1, 1, 0) signals['buy_signal_rule2'] = np.where(condition2, 1, 0) # 综合买入信号(任一规则触发) signals['buy_signal'] = signals[['buy_signal_rule1', 'buy_signal_rule2']].max(axis=1) # 风控规则:计算当日收盘价相对于持仓成本的回撤 (假设成本价为`entry_price`) # 这里需要一个外部输入的成本价,或者在回测/监控模块中计算 # signals['drawdown'] = (df['close'] - entry_price) / entry_price # signals['stop_loss_signal'] = np.where(signals['drawdown'] <= -0.08, 1, 0) # 卖出规则:收盘价跌破MA10 signals['sell_signal_ma'] = np.where(df['close'] < df['MA10'], 1, 0) # 标记信号发生日 signals['signal_date'] = signals.index return signals[['code', 'buy_signal', 'sell_signal_ma', 'signal_date']].dropna() # 使用示例 engine = SimpleSignalEngine() # 假设`df`是从数据库读取的某股票历史数据 signals_df = engine.generate_signals(df) print(signals_df[signals_df['buy_signal'] == 1].tail())避坑指南2:未来函数与数据穿透这是量化回测中最经典的坑!绝对不能用未来的信息做当前的决策。比如,在计算第T日的20日均线时,只能使用第T日及之前的数据。如果你不小心在第T日就用到了第T+1日的收盘价,那就是“未来函数”,回测结果会好得离谱,但实盘必然失效。在
pandas的rolling().mean()计算中,默认不会引入未来数据,但要警惕在数据对齐、拼接时可能出现的错位。我的检查方法是:在回测中,打印出信号产生当天的所有输入数据,人工核对一遍。
3.3 回测模块:用历史数据验证想法
信号规则写好了,但它真的有用吗?回测就是你的“历史实验室”。
# backtester.py class SimpleBacktester: def __init__(self, initial_capital=100000.0): self.initial_capital = initial_capital self.positions = {} # 持仓 {‘code’: {‘shares’: 100, ‘entry_price’: 10.0}} self.cash = initial_capital self.equity_curve = [] # 记录每日总资产 self.trade_log = [] # 记录每一笔交易 def run(self, data_df, signals_df): """运行回测,data_df包含价格,signals_df包含买卖信号""" # 按日期遍历 for date, daily_data in data_df.iterrows(): current_price = daily_data['close'] code = daily_data['code'] # 检查该日期是否有信号 daily_signals = signals_df[signals_df.index == date] if not daily_signals.empty: # 处理买入信号 if daily_signals.iloc[0]['buy_signal'] == 1 and code not in self.positions: # 简单策略:用50%的现金买入 max_shares = int((self.cash * 0.5) // (current_price * 100)) * 100 # A股一手100股 if max_shares >= 100: cost = max_shares * current_price self.cash -= cost self.positions[code] = {'shares': max_shares, 'entry_price': current_price} self.trade_log.append({ 'date': date, 'code': code, 'action': 'BUY', 'price': current_price, 'shares': max_shares, 'cash_after': self.cash }) # 处理卖出信号 if daily_signals.iloc[0]['sell_signal_ma'] == 1 and code in self.positions: position = self.positions.pop(code) revenue = position['shares'] * current_price self.cash += revenue profit = revenue - position['shares'] * position['entry_price'] self.trade_log.append({ 'date': date, 'code': code, 'action': 'SELL', 'price': current_price, 'shares': position['shares'], 'profit': profit, 'cash_after': self.cash }) # 计算当日总资产:现金 + 所有持仓市值 total_position_value = sum([pos['shares'] * current_price for pos in self.positions.values()]) total_equity = self.cash + total_position_value self.equity_curve.append({'date': date, 'equity': total_equity}) # 回测结束,平掉所有仓位 final_return = (self.cash - self.initial_capital) / self.initial_capital return pd.DataFrame(self.equity_curve), pd.DataFrame(self.trade_log), final_return # 使用示例 backtester = SimpleBacktester(initial_capital=100000) equity_curve_df, trade_log_df, final_return = backtester.run(df, signals_df) print(f"最终收益率: {final_return:.2%}") print(f"交易次数: {len(trade_log_df)}")避坑指南3:回测的“理想化”陷阱这个简单的回测忽略了很多现实因素:交易佣金和印花税、滑点(下单价格与实际成交价的差异)、涨停跌停无法买卖、停牌、资金分配冲突等。一个在回测中年化50%的策略,考虑这些因素后可能只剩10%。因此,回测结果一定要打折扣看。我的经验是,在初步回测表现不错后,必须加入至少千分之一的买卖佣金和0.1%的滑点假设,再跑一遍。如果策略依然稳健,才值得进一步考虑。
4. 让系统跑起来:监控、提醒与持续迭代
回测通过后,就可以搭建一个轻量级的监控系统了。这个系统将你的“交易宪法”应用到实时(或每日收盘后)的市场中。
4.1 构建每日监控脚本
这个脚本的核心任务是:定时运行,获取最新数据,计算信号,如果触发规则就通知你。
# monitor.py import schedule import time from datetime import datetime import pandas as pd from data_fetcher import StockDataFetcher from signal_engine import SimpleSignalEngine # 假设有一个通知发送模块 from notifier import send_wechat_notification class DailyMonitor: def __init__(self, watch_list): self.watch_list = watch_list # 监控的股票代码列表,如 ['sh600000', 'sz000001'] self.fetcher = StockDataFetcher() self.engine = SimpleSignalEngine() def job(self): print(f"{datetime.now()} 开始执行监控任务...") all_signals_today = [] for code in self.watch_list: # 1. 获取数据(例如,获取最近100天的数据用于计算指标) df = self.fetcher.fetch_daily_data(code, start_date=(datetime.now() - pd.Timedelta(days=100)).strftime('%Y%m%d')) if df.empty: continue # 2. 计算信号 signals = self.engine.generate_signals(df) # 3. 获取最新的信号(今天) latest_signal = signals.iloc[-1] if not signals.empty else None if latest_signal is not None and (latest_signal['buy_signal'] == 1 or latest_signal['sell_signal_ma'] == 1): signal_info = { 'code': code, 'date': latest_signal.name.strftime('%Y-%m-%d'), 'buy_signal': latest_signal['buy_signal'], 'sell_signal': latest_signal['sell_signal_ma'] } all_signals_today.append(signal_info) # 4. 实时发送通知(这里以打印代替) action = "买入观察" if latest_signal['buy_signal'] == 1 else "卖出警示" msg = f"[股票监控] {code} 于{latest_signal.name.date()}触发{action}信号。" print(msg) # send_wechat_notification(title="股票信号提醒", content=msg) # 实际调用通知函数 # 可以生成一个每日报告 if all_signals_today: report = pd.DataFrame(all_signals_today) report.to_csv(f"daily_signals_{datetime.now().date()}.csv", index=False) print(f"今日信号已保存至文件,共{len(all_signals_today)}条。") else: print("今日无信号触发。") print("监控任务完成。\n") def run(self): # 每天下午收盘后(如16:00)运行一次 schedule.every().day.at("16:00").do(self.job) # 或者每30分钟运行一次(用于盘中粗略监控,不推荐高频) # schedule.every(30).minutes.do(self.job) print("监控服务已启动,等待执行...") while True: schedule.run_pending() time.sleep(60) if __name__ == "__main__": # 监控列表最好控制在10-20只以内,太多则数据获取压力大,也看不过来 my_watch_list = ['sh600000', 'sz000001', 'sh601318'] monitor = DailyMonitor(my_watch_list) # 先手动执行一次,测试流程 monitor.job() # 如果需要定时后台运行,取消下面这行的注释 # monitor.run()4.2 通知渠道集成
让系统能“找到你”非常重要。除了打印到控制台,集成一个推送到手机的通知服务体验会好很多。
- 邮件通知:使用Python的
smtplib库,配置一个邮箱的SMTP服务。 - 微信/钉钉推送:使用
Server酱、Pushplus等第三方服务非常方便。它们通常提供一个Webhook URL,你只需要用requests库发送一个HTTP POST请求即可。
# notifier.py import requests def send_wechat_via_pushplus(token, title, content): """使用Pushplus发送微信通知""" url = 'http://www.pushplus.plus/send' data = { "token": token, # 在Pushplus官网获取 "title": title, "content": content, "template": "txt" } try: resp = requests.post(url, json=data, timeout=10) if resp.json().get('code') == 200: print("Pushplus消息发送成功") else: print(f"Pushplus消息发送失败: {resp.text}") except Exception as e: print(f"发送通知请求失败: {e}")4.3 系统的持续迭代:从“观察笔记”到“学习伙伴”
系统跑起来后,工作才刚刚开始。这个“小助手”最大的价值在于提供了一个客观的反馈循环。
- 记录与复盘:系统每天生成的信号日志和最终是否盈利的记录,是你最好的复盘材料。定期(比如每周、每月)回顾:哪些信号是有效的?哪些是无效的“噪音”?无效信号是否因为规则有漏洞?比如,你可能会发现“成交量放大”这个条件在震荡市中经常产生假信号。
- 规则优化:基于复盘,你可以调整你的“交易宪法”。例如,把“成交量大于1.5倍均量”改成“成交量大于1.2倍均量且连续两日放量”。然后,用历史数据重新回测,看优化后的规则是否在长期表现更好。切记:不要针对某一段特定行情过度优化(过拟合),要用更长的历史周期和样本外数据检验。
- 引入更复杂的“AI”元素:当简单的规则无法满足你时,可以尝试引入机器学习模型。例如,将多个技术指标(MA, RSI, MACD, 布林带宽度等)作为特征,将未来N日的涨跌作为标签,训练一个分类模型(如逻辑回归、随机森林、XGBoost)来预测买卖点。但务必清醒:这依然只是一个基于历史数据的概率模型,它的表现严重依赖于特征工程和市场环境,绝非稳赚不赔。你可以把它当作一个额外的、更复杂的“信号过滤器”,与你的基础规则结合使用。
- 心态的修炼:当系统发出与你直觉相反的信号时(比如让你止损),正是它发挥“管住手”作用的时刻。信任你的系统,前提是你已经通过充分的历史回测和模拟盘验证了它的长期逻辑。这个过程,本身就是对投资心态的极致锻炼。
5. 常见问题与进阶思考
在搭建和使用这样一个系统的过程中,你一定会遇到各种各样的问题。这里我总结几个最常见的,以及一些进阶方向的思考。
5.1 数据问题:准确性与延迟
- 免费数据的准确性:免费数据源偶尔会有错误,比如复权因子错误、价格异常等。对于关键交易,建议用多个数据源交叉验证,或者考虑付费购买更稳定、更准确的数据(如Wind、Choice金融终端的数据接口,但成本较高)。
- 实时数据延迟:如果你做的是短线或日内观察,免费数据的实时性(通常有几分钟到十几分钟的延迟)可能是致命伤。实盘交易需要Level-2行情或券商提供的实时API,这涉及到更高的成本和更复杂的编程。
5.2 策略失效与过拟合
- 市场风格切换:没有一个策略能永远有效。去年有效的“小盘股反转”策略,今年可能因为市场偏好大盘蓝筹而持续亏损。你的系统需要包含策略失效的检测机制。例如,当策略连续亏损达到一定次数或最大回撤超过某个阈值时,自动暂停该策略的信号,并提醒你重新评估市场环境。
- 过拟合的陷阱:这是回测中最诱人的魔鬼。当你不断调整参数,让策略在历史数据上画出完美的净值曲线时,很可能已经陷入了过拟合。防范方法:
- 样本外测试:将历史数据分为训练集(如2010-2018)和测试集(2019-2023)。只在训练集上优化参数,最终看测试集的效果。
- 简化规则:规则越复杂,参数越多,过拟合风险越大。坚持“奥卡姆剃刀”原则,如无必要,勿增实体。
- 多品种测试:一个好的策略应该在多只股票、多个行业上都有尚可的表现,而不是只对某一只股票有效。
5.3 从个人工具到AI Agent的遐想
当前的项目,本质上是一个规则驱动的自动化脚本。而“AI Agent”的概念更强调自主感知、决策和执行。对于个人投资者,我们可以朝这个方向做一些有趣的探索:
- 感知自动化:除了股价,能否自动爬取相关的新闻、财报摘要、社交媒体情绪,并用NLP技术进行情感分析,生成一个“市场情绪指标”作为输入?
- 决策多样化:不再依赖单一策略,而是维护一个“策略池”。系统可以根据当前的市场波动率、趋势强度等宏观状态,自动选择最适合的当前策略(比如高波动用均值回归,强趋势用动量跟踪)。
- 执行优化:在决定买卖后,能否设计更优的执行算法?比如,将一个大订单拆分成多个小单,在一定时间内分批下单,以降低市场冲击成本。
这些想法实现起来复杂度呈指数级上升,但对于学习者而言,每一个小点都可以作为一个独立的项目去钻研。例如,单独做一个“基于文本情感的股票信号辅助系统”,就非常有挑战性和价值。
最后,我想说的是,这个“AI股票小助手”项目,其价值远不止于代码本身。它更像是一面镜子,逼着你将模糊的投资理念具象化为清晰的规则,用冰冷的历史数据检验你的一厢情愿,用程序的纪律对抗人性的贪婪与恐惧。在这个过程中,你收获的将不仅仅是一套代码工具,更是一种理性、系统化的市场观察和决策方式。从这个角度看,每一个试图用代码管住自己手的普通人,都已经在投资的路上,向前迈出了最关键的一步。