直接开门见山。乌龟交易系统,这个源自Richard Dennis和William Eckhardt在1980年代搞出来的趋势跟踪策略,直到今天依然是量化圈子里的经典入门必修课。但大部分人拿到海龟法则,都是在Jupyter Notebook里跑个回测、画个净值曲线就结束了,离真正能“用”还有一段距离。我这次想做的,不是再写一遍策略,而是基于Python和Django,把海龟策略的整个流程——信号扫描、仓位管理、止损止盈、交易日志——沉淀成一个真正能日用的管理系统。
这篇文章适合谁看?一种是已经懂海龟策略、但想把策略工程化、产品化的开发者,另一种是刚学完Django基础、想找个有含金量的实战项目来练手的朋友。无论你是哪一类,这篇文章都会从需求拆解讲到代码落地,再讲到部署避坑。我会先聊清楚系统架构和数据模型怎么设计,再给出关键代码实现,最后把我在开发过程中踩过的坑、排查过的性能问题一次性都翻了底朝天。
1. 项目整体设计与核心需求拆解
1.1 乌龟交易法则的核心逻辑回顾
很多人一提到海龟交易,脑子里就浮现出“20日突破买入、10日突破卖出”这两条简单规则。没错,这是海龟系统的入场逻辑,但完整的海龟法则远不止这一点。它包含了一整套资金管理框架:以ATR(真实波动幅度)作为仓位和止损的统一度量单位,用N值计算每笔交易的头寸规模,单笔风险控制在账户权益的2%以内,同时采用“分批建仓、金字塔加仓”的方式,在趋势延续时逐步放大仓位,而在价格回撤达到一定幅度时逐步减仓离场。
用大白话解释就是,海龟系统不是靠预测涨跌赚钱的,而是靠“抓住大趋势+严格控住亏损”这两条腿走路。入场后对了就拿着,错了就按规则止损,绝不主观犹豫。这一套逻辑放到软件系统里,天然就适合拆分成几个独立模块:行情数据获取、信号计算、仓位计算、订单管理、风控检查。这也就是我要用Django来实现的核心骨架。
1.2 为什么管理系统比单跑策略更“值钱”
如果只是在Notebook里调现成的talib库,写个双均线交叉信号,再算算累计收益,两三个小时就能搞定。但这种方式有一个致命问题——策略永远停留在“回测通过”的阶段,离真实可操作还隔着十万八千里。真实场景下,我们每天要面对的是:今天有没有触发信号?如果触发了,用多大的仓位进场?账户里当前挂着多少敞口?上一次止损之后,系统有没有正确重置状态?
这些问题的答案,是需要一个持续运行、能记录状态、能追踪历史的管理系统来回答的。Django在这个场景下派上了大用场。它的ORM帮你把策略状态、交易记录、账户快照持久化到数据库,它的自带Admin后台在开发阶段就能快速可视化交易数据,而它的迁移机制、信号机制和中间件体系,让你可以像一个真正的产品团队一样去组织代码和迭代功能。
我在设计这个系统时,定的原则很简单:策略引擎和业务模型要彻底分离。策略引擎是纯Python模块,不依赖Django的任何Features,这样以后想换FastAPI甚至CLI脚本都能复用。而Django这一侧,只管接收、存储、展示和调度,相当于一个“总控台”。
1.3 典型用户与使用场景
这个系统到底给谁用?我的答案是两类人。第一类是个人量化交易者,尤其是做期货、趋势跟踪策略的,他们可以用这个系统替代手工盯盘、用Excel记交易的方式,每天自动化扫描信号,再按系统提示下单和调整仓位。第二类是培训机构或自学者,把海龟系统作为教学案例,演示如何把策略思路转成工程化系统。
我在开发时设定了两个核心使用场景。第一个是每日收盘后的批量扫描:系统自动拉取最新行情,计算ATR、突破位、当前持仓状态,推送当日信号列表。第二个是交易记录追踪:每次下单后,手动或自动录入成交价、手数、时间,系统自动计算当前浮动盈亏、剩余可用头寸单位、止损价位置。这两个场景覆盖了海龟策略从“发现机会”到“离场结算”的全周期。
1.4 技术选型背后的理由
既然标题是“Python基于Django”,Django自然是主框架,但这不代表所有组件都得是Django的。我最终的选型是:Django 4.2作为Web应用框架、Django ORM(配PostgreSQL)做持久化、Celery + Redis做定时任务和异步扫描、Bootstrap 5做前端界面(Django模板渲染)、plotly.js做图表。这套组合的取舍逻辑很实际:Django生态成熟,ORM和Admin能显著减少重复工作;Celery是为了让行情扫描这种耗时任务不阻塞Web请求;PostgreSQL是因为我要用到数组字段和JSON字段来存储信号快照,这在MySQL里写起来没那么顺手。
有一点要提醒:Django的项目级实战,真正复杂的不是代码本身,而是数据流的设计。信号从行情扫描模块进来,需要写入信号表,关联到当日的市场状态;交易从订单表进来,需要联动持仓表、资金表和绩效表。如果一开始表结构设计不合理,后面改起来非常痛苦。我为此重新设计了三次数据库模型,后面会详细讲最终版的表结构和字段含义。
2. 数据库模型设计与核心业务实现
2.1 实体关系与表结构设计
我最终定稿的数据库模型围绕五个核心实体:Market(品种)、PriceBar(日线行情)、Signal(交易信号)、Position(持仓记录)、TradeLog(交易流水)。
品种表很简单,字段包括代码、名称、市场类型、合约乘数、最小变动价位、每手大小。这些基础信息是仓位计算必需的。比如你交易螺纹钢,合约乘数是10吨/手,最小变动价位是1元,那么一个点的盈亏就是10元。如果连合约乘数都不存,仓位计算就是无源之水。
行情表存储每日的OHLCV数据,我特意加了atr字段,直接在行情入库时一并计算,避免每次回测或扫描时重复计算ATR。信号表的字段最复杂,包含了信号类型(多头/空头)、触发日期、入场价格、入场时机描述、系统类型(System1或System2)、有效状态(未处理/已处理/已过期)、关联的当日ATR值。持仓表记录当前未平仓头寸,包括开仓时间、开仓价格、当前手数、已加仓次数、止损价格、初始止损价格。交易流水表则记录每一次平仓或平今的操作,便于后期做绩效归因。
2.2 Django模型代码与关键字段解释
模型的实现我直接贴核心代码,注释我会写得比较细。
from django.db import models from django.utils import timezone from decimal import Decimal class Market(models.Model): symbol = models.CharField('合约代码', max_length=20, unique=True) name = models.CharField('品种名称', max_length=50) exchange = models.CharField('交易所', max_length=20) multiplier = models.DecimalField('合约乘数', max_digits=12, decimal_places=2, default=10) min_tick = models.DecimalField('最小变动价位', max_digits=12, decimal_places=4, default=1) tick_value = models.DecimalField('每跳盈亏', max_digits=12, decimal_places=4, blank=True) def save(self, *args, **kwargs): self.tick_value = self.multiplier * self.min_tick super().save(*args, **kwargs) def __str__(self): return f"{self.name}({self.symbol})" class PriceBar(models.Model): market = models.ForeignKey(Market, on_delete=models.CASCADE, related_name='bars') date = models.DateField() open = models.DecimalField('开盘', max_digits=12, decimal_places=4) high = models.DecimalField('最高', max_digits=12, decimal_places=4) low = models.DecimalField('最低', max_digits=12, decimal_places=4) close = models.DecimalField('收盘', max_digits=12, decimal_places=4) volume = models.BigIntegerField('成交量') atr14 = models.DecimalField('ATR14', max_digits=12, decimal_places=4, null=True, blank=True) class Meta: ordering = ['date'] indexes = [ models.Index(fields=['market', 'date']), models.Index(fields=['date']), ] unique_together = ('market', 'date') def __str__(self): return f"{self.market.symbol} {self.date} {self.close}"Markets里我用Decimal而不是Float,这是因为资金计算和价格计算对精度极其敏感。你可能觉得浮点数差个零点零零几无所谓,但当你计算加仓预算、保证金占用、浮动盈亏时,一个小数尾差就可能导致保证金不足或下单手数错误。这个坑在金融系统里算是老生常谈,但我仍然要强调,所有涉及金额和价格的字段一律用Decimal,否则早晚会被浮点误差坑到。
PriceBar表里的unique_together非常关键,它保证了同一品种同一天不会出现两条行情记录。定时任务扫重复数据时,可以直接用update_or_create来避免主键冲突。
2.3 持仓与资金表的联动设计
持仓表我单独拿出来讲,因为这是整个系统状态流转的核心。
class Position(models.Model): SIDE_CHOICES = [ ('LONG', '多头'), ('SHORT', '空头'), ] STATUS_CHOICES = [ ('OPEN', '持仓中'), ('CLOSED', '已平仓'), ('PENDING', '待开仓'), ] market = models.ForeignKey(Market, on_delete=models.CASCADE, related_name='positions') opened_at = models.DateTimeField(default=timezone.now) closed_at = models.DateTimeField(null=True, blank=True) side = models.CharField(max_length=10, choices=SIDE_CHOICES) status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='PENDING') units = models.IntegerField('当前单位数', default=0) entry_price = models.DecimalField('入场均价', max_digits=12, decimal_places=4) current_stop = models.DecimalField('当前止损价', max_digits=12, decimal_places=4) initial_stop = models.DecimalField('初始止损价', max_digits=12, decimal_places=4) max_add_units = models.IntegerField('最大加仓次数', default=4) added_units = models.IntegerField('已加仓次数', default=0) def unrealized_pnl(self, current_price): price_diff = current_price - self.entry_price if self.side == 'SHORT': price_diff = -price_diff return price_diff * self.market.multiplier * self.unitsunrealized_pnl方法是持仓表的核心,它接收当前价格,直接返回浮动盈亏。这个方法的计算逻辑其实就一行,但它被大量复用在Admin后台、用户页面、风控检查、绩效报表四类场景里。Django模型的自定义方法让我不必在多个视图里重复抄计算逻辑,这也是ORM用得好能明显减少冗余代码的地方。
资金表我用了稍微激进一点的设计——把账户资金快照按日存储,而不是只存一个实时值。因为汇率调整、手续费扣除、浮盈浮亏的变化如果只保留当前状态,你根本没法复盘一周前为什么回撤那么大。资金日表每天收盘后自动生成一条快照,包含当日权益、累计盈亏、回撤比例、可用保证金、已用保证金,这样绩效分析和风险复盘都有了时间序列数据支撑。
2.4 为什么要给信号表加“状态机”
信号表不能只记录“今天突破了、建议做多”,因为一个信号从触发到执行,中间还隔着人的决策、交易所的交易时间、跳空开盘等不确定性。我加了一个status字段,生命周期是:CREATED(已生成) → CONFIRMED(已确认待下单) → EXECUTED(已执行) → EXPIRED(已过期) → IGNORED(已忽略)。
这个状态机是几次实战教训换来的。最早版本里信号表只有“是/否”两个状态,结果遇到一次夜盘跳空高开,系统生成的多头信号价格远低于开盘价,如果蠢到直接按信号价进场,等于在最高的位置接盘。后来我加入了“确认”机制,用户每天早上看信号列表,手动确认哪些信号执行、哪些跳过,自动化辅助决策但保留人的否决权。这个设计对注重风控的个人交易者非常重要,真正优秀的交易系统从来不是全自动无脑执行,而是把人的纪律和机器的计算能力结合起来。
3. ATR计算与信号扫描模块的实现细节
3.1 手写ATR计算的正确打开方式
ATR(Average True Range)是海龟系统的地基。ATR在这里不是用来做技术指标装饰花的,它是仓位计算、止损设置、突破判定三者的统一长度单位。理论上你有两种实现方式:调talib的ATR函数,或者自己手写。我的建议是手写,因为自己写才能彻底理解这个指标的计算过程,而且可以在计算过程中顺便校验数据质量。
ATR的计算分两步。第一步,计算每个交易日的真实波幅(True Range,TR)。TR是以下三个值中的最大值:当日最高价减当日最低价、当日最高价减前一日收盘价的绝对值、当日最低价减前一日收盘价的绝对值。第二步,对TR取N日简单移动平均。海龟系统的经典参数是N=20,所以A就是20日TR的均值。
def calculate_atr(bars, period=20): if len(bars) < period + 1: return Decimal('0') tr_list = [] for i in range(1, len(bars)): high = float(bars[i].high) low = float(bars[i].low) prev_close = float(bars[i - 1].close) tr = max( high - low, abs(high - prev_close), abs(low - prev_close) ) tr_list.append(tr) if len(tr_list) < period: return Decimal('0') atr = sum(tr_list[-period:]) / period return Decimal(str(atr))这段代码的逻辑并不复杂,但有几个细节值得注意。第一,第一个交易日前没有前一日收盘价,所以TR序列是从第2根K线开始的。第二,我用的是简单均值而非平滑均值,虽然很多技术指标库用的是Wilder平滑,但海龟原版规则用的就是简单移动平均,翻《海龟交易法则》附录可以验证。第三,如果数据不足20日,返回0比抛异常要好,因为在批量扫描时,有些新品种上市时间短,数据量天然不足,这个情况应该被记录而非中断。
3.2 突破信号判定与System1/System2双系统
海龟系统里有两套入场系统,参数不同,目的也不同。System1采用20日突破入场、10日突破离场,对应的是中短周期趋势跟踪;System2采用55日突破入场、20日突破离场,对应的是长周期趋势跟踪。两套系统叠加运行,等于同时捕捉中速和慢速两类趋势行情。
用Django ORM实现突破判定,核心就是一条查询:找出当前收盘价大于(或小于)前20日(或55日)最高价(或最低价)最高值的记录。
def check_breakout(market, bars, lookback=20, breakout_type='high'): if len(bars) < lookback + 1: return False, None recent_bars = bars[:-1] # 排除当日 if breakout_type == 'high': threshold = max(float(b.high) for b in recent_bars[-lookback:]) return float(bars[-1].close) > threshold, threshold else: threshold = min(float(b.low) for b in recent_bars[-lookback:]) return float(bars[-1].close) < threshold, threshold这里最关键的设计是bars[:-1],也就是用当日之前的数据去计算突破位。为什么不是包含当日?因为盘中最高价、最低价是动态变化的,你不可能用还没收盘的数据去触发一个收盘确认的信号。正确的海龟玩法是收盘后确认收盘价是否突破前N日的极值。这个“未来函数”的坑,量化新手最容易踩——回测时把当日数据加入突破位计算,结果信号极其频繁,实盘一跑就废了。
System1和System2的差异不只是参数不同,还有一套复杂的“上次同方向触发是否盈利”过滤器(海龟称之为System1过滤器),但这个过滤器在工程实现上比较复杂,我初版没有实现。理由是:它依赖每一笔已平仓交易的盈亏结果来动态调整当前交易日的信号有效性,这个状态在数据库里要额外记录“上次信号结果”,对数据库更新频率要求很高。我建议先把基础版本跑通,确认数据流和页面展示没问题,再逐渐增加过滤规则,工程上的推进节奏很重要。
3.3 Celery定时任务与每日自动扫描
信号扫描不能每天手动点按钮,尤其当你有十几个交易品种时,每次扫描要计算每个品种的ATR、20日突破、55日突破、当前持仓状态,串行跑可能要十几秒。这个任务放在Web请求里显然不合适,所以我用Celery的定时任务,每天收盘后(比如交易日下午15:10)自动执行一次全市场扫描。
CELERY_BEAT_SCHEDULE = { 'scan_daily_signals': { 'task': 'trading.tasks.scan_daily_signals', 'schedule': crontab(hour=15, minute=10, day_of_week='mon-fri'), }, }扫描任务的任务核心逻辑就是遍历每个品种,读取最近60根K线,计算ATR,判断两个系统的突破信号,如果有信号则生成Signal记录。这里有一个容易被忽略的问题:交易所有中午休市和夜盘交易,K线数据的结束时间点不同。如果你直接用自然日分组,节假日前后可能串数据。我的做法是按交易日历去重,而不是按自然日去重,因为行情接口返回的数据里通常带trade_date字段,按这个字段去重才准确。
3.4 用Django Admin做手动干预出口
虽然我们写了自动化扫描,但一个完整的管理系统必须保留人工干预的入口。Django Admin作为“穷人版管理后台”在这里恰好够用。我在Admin里注册了所有模型,并重写了几个关键方法:
@admin.register(Signal) class SignalAdmin(admin.ModelAdmin): list_display = ('market', 'signal_type', 'system', 'trigger_date', 'entry_price', 'status') list_filter = ('status', 'signal_type', 'system') actions = ['confirm_selected'] @admin.action(description='确认选中信号') def confirm_selected(self, request, queryset): queryset.update(status='CONFIRMED')Admin里列表页、筛选条件、批量操作这三个特性,几乎不费吹灰之力就实现了交易信号的管理界面。很多Django教程讲Admin都只讲注册模型,实际上Admin的可扩展性远比想象中强。你把list_display、list_filter、actions用起来,就等于做了一套能处理的业务后台,这在个人项目和创业公司早期阶段非常够用。
4. 仓位计算与止损风控功能的工程实现
4.1 用N值计算头寸数量的完整逻辑
仓位管理是海龟系统区别于普通趋势策略的核心。海龟法则规定,单个品种的仓位大小由该品种的N值和账户权益共同决定。每一单位的头寸规模计算公式是:
单位数量 = (账户权益 × 1%) ÷ (N × 合约乘数)
注意这里用的不是2%的风险比例,而是1%。因为后续还有加仓逻辑,每次加仓都用0.5个N的间距去递增,单笔交易的累计风险会被放大到约2%。用1%作为基础单位,后续加仓后的总风险才刚好控制在2%。
这个计算逻辑直接写成一个独立的函数,同时接受账户权益和ATR作为参数:
def calculate_units(account_equity, atr_value, market, risk_percent=1.0): if atr_value == 0: return 0 risk_amount = account_equity * Decimal(risk_percent / 100) unit_value = atr_value * market.multiplier units = int(risk_amount / unit_value) return max(units, 1)这里用int()向下取整,是因为交易手数必须是整数,不能出现0.5手这种实际不存在的数量。向下取整比四舍五入保守,宁可少开一手也不能让单笔风险暴露超过计划值。
4.2 金字塔加仓与止损位上移的算法实现
海龟的加仓规则是以初始入场价格为基准,每上涨(做多时)0.5个ATR就加一个单位,最多加到4个额外单位。每次加仓后,止损位也相应上移0.5个ATR。这个“移动止损”的用途就是保护浮盈——趋势继续走,止损跟着走;趋势突然反转,系统在成本线上方或至少不亏大钱的位置离场。
class PositionManager: def __init__(self, position, current_atr): self.position = position self.current_atr = current_atr def should_add_at(self, current_price): if self.position.added_units >= self.position.max_add_units: return False add_distance = self.current_atr * Decimal('0.5') if self.position.side == 'LONG': target = self.position.entry_price + add_distance * (self.position.added_units + 1) return current_price >= target else: target = self.position.entry_price - add_distance * (self.position.added_units + 1) return current_price <= target def update_stop(self): if self.position.side == 'LONG': new_stop = self.position.entry_price + self.current_atr * Decimal('0.5') * self.position.added_units else: new_stop = self.position.entry_price - self.current_atr * Decimal('0.5') * self.position.added_units if self.position.side == 'LONG': if new_stop > self.position.current_stop: self.position.current_stop = new_stop else: if new_stop < self.position.current_stop: self.position.current_stop = new_stop这段代码里最需要留意的是update_stop里那个比较判断:止损只能向有利方向移动,不能因为ATR变小就把止损往回拉。ATR作为波动率指标,本身是动态变化的,趋势行情中波动加剧,ATR变大;震荡行情中波动收窄,ATR变小。如果止损位允许随ATR变小而回撤,那等于给了趋势反转更多亏损空间,这违背了海龟系统“截断亏损、让利润奔跑”的核心原则。
4.3 风控模块与仓位调整的联动
当多个品种同时触发信号时,海龟系统还要求总体仓位上限——同一时间最多持有一定数量的“市场单位”,而且单个市场的单位数量不能超限。这个风控逻辑其实就是一个持仓汇总查询:
def current_total_units(user): open_positions = Position.objects.filter( status='OPEN' ) total_units = sum( p.units * (1 if p.side == 'LONG' else -1) for p in open_positions ) return total_units这个函数可以在每次加仓前调用,判断当前账户整体敞口。如果总单位数超过上限(比如海龟原版规定不超过12个单位,同一方向不超过4个单位),就拒绝新的加仓指令。这种在开仓、加仓前统一做风控检查的做法,比在每个视图里单独写判断要优雅得多,也方便以后把风控策略拆成独立的配置项。
4.4 资金曲线的实时展示与K线联动
仓位算完了,信号也确认了,管理系统还缺一个直观的驾驶舱页面。我用plotly.js在Django模板里渲染了三个核心图表:账户资金曲线、各品种的当前浮动盈亏分布、K线图上叠加的入场点和止损线。
资金曲线直接用资金日表的时间序列数据绘制,K线图用plotly的candlesticktrace。我特意实现了“在K线图上叠加止盈止损线”的功能——把持仓记录的current_stop值作为一条水平虚线画在图上,这样你一眼就能看出当前价格离止损还有多远。这个功能在开发时增加了不少工作量,但实际使用中价值极高,比看一堆冷冰冰的数字直观得多。
5. 数据接入、信号测试与部署
5.1 行情数据接入方案对比
Django本身不生产数据,它只是数据的搬运工和管理员。行情数据从哪里来?我在开发中对比了三种方案。
第一是Tushare Pro,数据质量好,字段全,但注册门槛高,积分要求越来越多,很多基础接口的权限都对低积分用户关闭。第二是Baostock,免费、免注册、直接用,日线数据、分钟数据都有,格式对Django来说也够用。第三是自己爬交易所官网,维护成本高,不推荐。最终我选的是Baostock,理由很简单:免费、无积分门槛、数据量对日线策略足够。BAOSTOCK取数据的过程就不再贴完整代码了,核心是先把历史数据一次性灌入,再用Celery每日增量更新收盘数据。
这里要提醒:数据源的复权问题。baostock默认返回的是前复权数据,海龟趋势跟踪策略如果做历史回测,必须统一复权方式。如果混用不复权和前复权的数据,计算出来的ATR和突破位会严重失真。我最终选择了“不复权”的原始数据,因为实盘交易时你在行情软件里看到的也是不复权价格,这样信号和实际屏幕价格之间没有隐含的差异。
5.2 信号准确性的验证方法:双轨测试
写完了信号扫描模块,你绝对不能直接上实盘,至少要做一个“模拟盘双轨测试”。我的做法是在同一个数据库里开一张paper_trade_log表,与真实交易日志的结构完全相同,但标记为PAPER。系统每天扫描生成的信号,统一记入模拟盘;我把模拟盘运行了一个月,与真实盯盘结果对比,确认信号触发率、方向准确率,尤其是要确认有没有因为数据源错误导致“假突破”。
双轨测试还有一个额外好处:它可以验证仓位计算是否合理。模拟盘里按海龟公式计算出单位数量,再用模拟的资金账户去做盈亏核算,看最大回撤是否在可接受范围内。这个过程可以帮你发现一系列工程Bug——比如ATR计算时索引越界、突破阈值取错K线范围、加仓逻辑中重复触发等。
5.3 Linux服务器部署:uWSGI + Nginx + PostgreSQL
系统本地开发没问题后,还有最重要的一步:部署到服务器上持续运行。我的部署方案是基于Linux + Nginx + uWSGI + PostgreSQL的经典组合。Django的部署其实不复杂,核心就两条:用Nginx处理静态文件和反向代理,用uWSGI启动Django应用。
部署时我踩过一个不小的坑:Celery的beat调度在服务器上常驻内存,如果服务器重启,不会自动拉起。你必须把Celery和Celery Beat注册为systemd服务,设置开机自启和崩溃自动重启。只部署Django而忘了Celery,会导致行情扫描和信号推送在服务器重启后彻底罢工,而且不会有人立刻发现。
另一个性能调优点是数据库索引。行情表数据增长很快,三个月的数据就有几十万行,如果不加索引,每日扫描时filter(market=xxx, date__gte=xxx)这类查询会非常慢。我在模型设计阶段就给行情表加了复合索引,这一点之前已经体现,实际效果明显,扫描任务从最初的四五秒缩短到几百毫秒。
5.4 常见问题与排查心得
开发过程中我整理了一份高频问题清单,遇到问题直接对照排查,能省不少时间。
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 信号从未触发 | 突破判定用了当日数据包含 | 检查判定函数是否排除bars[-1] |
| ATR为0 | 历史数据不足20根K线 | 在扫描任务里打印跳过原因 |
| 加仓后止损反而降低 | update_stop缺少方向判断 | 检查止损只向有利方向移动 |
| 每日扫描任务不执行 | Celery Beat未注册systemd | 检查systemctl status celery-beat |
| 查询行情极慢 | 缺少市场+日期复合索引 | 用explain分析查询计划 |
| 资金是负数 | 浮点数运算导致精度丢失 | 全部金额字段换成Decimal |
| 信号触发价与盘面不一致 | 复权方式不统一 | 所有数据统一用前复权或全部不复权 |
最后再分享一个我的心法。金融量化系统的项目,不要一上来就堆功能。先把“行情入库 → 信号扫描 → 人工确认 → 仓位计算 → 交易记录 → 绩效展示”这条主干路打通,再逐步加风控规则、加自动化推送、加模拟盘。主干通了,系统才是个能用的活物,否则代码再多也只是个半成品展示品。乌龟交易系统本身就是一套很简单但极其严密的规则,工程上也是一样,把最核心的流程做成稳如磐石,比炫技式的复杂架构有价值得多。