
群里有人甩过来一句matlabk大于等于0如何表示底下紧跟着另一个链接叫股票分享课一如何挑选白马股乍一看是两件八竿子打不着的事一个是写代码时的语法细节一个是看财报时的选股方法。但我盯着这个拼接标题看了一会儿反而觉得它挺有意思MATLAB 里写k 0是给数据加一个判断条件挑选白马股本质上也是给一堆公司加一串判断条件只不过前者判断的是数值后者判断的是财务指标。这篇就顺着这个思路往下聊前半段把 MATLAB 中k 大于等于 0这件事从语法到浮点精度坑全部拆干净后半段把这套条件筛选的思维方式迁移到白马股的量化初筛上给出可以直接抄的代码框架和参数设定思路。适合两类人看写 MATLAB 时被逻辑索引和浮点比较坑过的同学以及想用一点工程化手段管理自己选股清单的普通投资者。代码部分我会尽量写到能直接跑选股部分只讲方法论和筛选框架不碰任何具体标的。1. 先把标题拆开看两个问题共用一套判断逻辑1.1 MATLAB 里 k 大于等于 0 的几种写法与取舍先从最直白的说起。在 MATLAB 里表达k 大于等于 0最常用的就是关系运算符写法k 0它返回一个 logical 类型的值标量场景下就是true或false。除此之外还有函数式写法ge(k, 0)两者在结果上完全等价区别只在于可读性和使用场合。我个人的习惯是在普通表达式里用k 0在需要把比较操作作为一个函数句柄传出去的时候才用ge比如arrayfun((x) ge(x,0), arr)这种写法虽然不推荐但读别人代码时会遇到。真正需要留心的是不等于负数这种反向表达。新手常写成~(k 0)逻辑上没错但在处理 NaN 的时候结果会出乎意料k 0对 NaN 返回false取反之后就是true于是 NaN 被你当成了非负数。而k 0对 NaN 直接返回false。这两条路径对缺失值的态度完全相反一旦你的数据里有 NaN选哪种写法会直接改变最终结果。所以在做数据清洗时我一般先用isnan(k)把缺失值单独拎出来处理再谈大小比较。写法返回类型对 NaN 的结果适用场景k 0logicalfalse常规判断推荐默认使用ge(k, 0)logicalfalse需要函数句柄或函数式调用~(k 0)logicaltrue不推荐NaN 语义反直觉k - abs(k) 0logicalfalse数学等价但性能差仅作理解sign(k) 0double/logical视版本不推荐多一次运算且语义模糊上面这张表我建议存下来尤其是第三行那个坑我在帮别人看代码时至少遇到过五六次问题表现都是明明数据没问题筛选结果就是多出几条。1.2 白马股的筛选本质上就是一串布尔条件再看标题的后半截。所谓挑选白马股翻译成工程语言就是给定一张包含若干公司财务数据的表用一组条件把不符合的筛掉剩下的进候选池。这个过程和 MATLAB 里的逻辑索引是同一个动作。你在代码里写pool T(T.ROE 0.15 T.DebtRatio 0.5, :)和你在一张 Excel 表里手动勾选净资产收益率大于 15% 且 资产负债率小于 50%在数学上是完全一样的东西。区别只在于手工筛选靠眼睛和记忆量一大就失控用代码筛选可以把条件写下来、存起来、每次用同一套标准执行还能回看上次为什么把这家排除了。这就是我为什么觉得把这两个话题拼在一起讲并不违和——它们共同的底层能力是把一个模糊的判断标准拆解成若干个可以用数值比较表达的布尔条件。这件事做好了写代码和做筛选都会清爽很多。当然股票这边比代码多了一层麻烦代码里的k是确定的财报里的数字是估计的、会追溯调整的、口径可能不一致的。所以后半段我会花不少篇幅讲数据清洗和阈值校准这也是实际做过的人才知道最耗时间的部分。1.3 把两件事放在一起讲的实际好处有人可能会问硬把两个话题凑一块是不是为了蹭关键词。坦白说标题本身确实像是搜索词拼接但我确实从中发现了一个实用的迁移在 MATLAB 里养成先定条件、再做索引、最后复核边界的习惯直接就能套到选股流程上。写代码时你会检查k 0在边界值 0 上是否正确做筛选时你同样要检查负债率刚好 50% 的公司算不算通过这种边界问题。而且两边都有硬阈值 vs 打分排序的取舍。代码里你可以用all(cond)做严格全通过也可以用加权打分挑前 N 个选股里同样可以在全部指标达标和综合得分排名之间选。这个思路的迁移非常自然后面第 3 章会详细展开。2. MATLAB 条件判断的实操细节2.1 标量、向量、矩阵同一个符号的三种行为k 0这个表达式最容易被低估的地方是它的返回结果随k的形状变化。当k是标量时返回单个 logical当k是向量时返回同长度的 logical 向量当k是矩阵时返回同尺寸的 logical 矩阵。这一步是逐元素比较不是整体比较。很多人第一次写if k 0而k是个向量时会收到条件表达式必须为标量的报错就是这个原因。要判断向量里所有元素都大于等于 0正确写法是all(k 0)判断存在至少一个用any(k 0)判断全部小于 0用all(k 0)或者更明确地写~any(k 0)。这里有个语义细节值得强调all(k 0)和~any(k 0)在正常情况下等价但遇到 NaN 时又分叉了因为k 0和k 0对 NaN 都返回 falseall(false)是 false~any(false)是 true。所以处理可能含 NaN 的数据时我倾向于统一用all(k 0)这种正向判断的写法语义更清楚。如果只想把非负元素取出来逻辑索引是k(k 0)这个写法非常常用。反过来取负元素是k(k 0)。需要注意k(k 0)返回的是列向量即使k原本是行向量这一点在拼接矩阵时经常导致维度错误加个reshape或者转置就能解决。2.2 浮点数比较k 0 为什么会漏判这是我在实际项目里踩得最深的一个坑。假设你有一列数据是通过一系列加减乘除算出来的理论上某个值应该正好是 0但由于二进制浮点表示的限制实际存下来可能是-2.2204e-16。这时候k 0返回 false你的筛选就把它排除了然后你盯着结果百思不得其解。浮点数的本质是十进制里能精确表示的数二进制里不一定能。0.1 0.2 在 MATLAB 里不等于 0.3而是 0.30000000000000004这是 IEEE 754 双精度的固有特性不是 MATLAB 的 bug。所以任何涉及浮点运算结果的比较都不应该用严格的、而要引入容差。标准做法是定义一个容差tol把大于等于 0放宽成大于等于 -toltol 1e-9; mask k -tol;容差取多大要看你的数据量级。如果k的量级在 1e6 左右双精度有效位数约 15 到 16 位那么绝对容差取 1e-6 到 1e-8 比较合理如果k本身量级很小比如 1e-8用绝对容差就没意义了应该用相对容差tol eps(max(abs(k))) * n。MATLAB 提供eps函数给出机器精度eps(x)返回 x 到下一个双精度数的距离用它来构造相对容差是比较稳妥的。还有一个更隐蔽的场景k是整数类型int32、uint8 之类。整数之间的比较没有精度问题但如果k是无符号类型k 0永远为 true因为无符号数根本不可能是负数。这时候如果你的判断逻辑依赖负数存在这件事代码就静默失效了。我在处理图像数据时遇到过uint8相减结果被截断为 0怎么减都是非负排查了半天。2.3 复数、NaN、整数类型三个隐蔽陷阱复数是被最多人忽略的一个。MATLAB 里对复数使用时只比较实部虚部完全被忽略。也就是说3100i 0返回 true。如果你在做信号处理或特征值计算变量理论上应该是实数但因为数值误差带了极小虚部用k 0判断看起来能用实际上是在悄悄丢弃虚部信息。稳妥的写法是先检查虚部或直接取实部if isreal(k) || all(abs(imag(k(:))) 1e-12) tf real(k) -tol; endNaN 的问题前面提过这里补充一个实用技巧all(k 0)在有 NaN 时返回 false这个行为其实是对的因为存在一个未知值不应该被判定为全部满足。但如果你的业务逻辑是忽略缺失值只看有效数据那就要先k k(~isnan(k))再判断或者用all(k(~isnan(k)) 0)。这两者的区别在于前者把有缺失当作不通过后者把缺失值跳过。选哪种取决于你的数据契约一定要写清楚别让后面接手的人猜。整数类型的坑还有一处不同整数类型之间不能直接比较MATLAB 会报错要求类型一致。int8(5) int16(0)会提示整数类型不匹配需要显式转换。这在读二进制文件或者硬件采集数据时很常见。2.4 把判断封装成可复用函数写多了就会发现判断是否非负这个动作在不同项目里反复出现而且每次都要考虑容差、NaN、复数。我的做法是封一个小函数把这些边界情况一次性处理掉function tf isNonNegative(k, tol) % ISNONNEGATIVE 判断输入是否全部非负考虑浮点容差与缺失值 % k : 数值数组任意维度 % tol : 绝对容差默认 0 % 返回 logical 标量 if nargin 2 || isempty(tol) tol 0; end k k(:); % 复数虚部必须可忽略否则不参与比较 if ~isreal(k) if any(abs(imag(k)) 1e-12) tf false; return; end k real(k); end % 缺失值剔除后再判断 k k(~isnan(k)); if isempty(k) tf true; % 空集视为满足按业务需要可改为 false return; end tf all(k -abs(tol)); end这段代码里有几个决定是我特意做的值得说一下。第一把输入强制拉成一维避免矩阵场景下all默认按列操作带来的歧义第二复数虚部超过阈值直接判否而不是悄悄取实部避免掩盖上游计算问题第三空集返回 true这是空真vacuous truth的语义数学上成立但如果你的业务里没有数据应该算不通过就改掉这一行。这种小函数的价值不在于省几行代码而在于把边界条件的处理决定固化下来写一次想清楚以后不用每次重新纠结。3. 从代码条件到选股条件把白马翻译成指标3.1 白马股这三个字到底指什么先把概念说清楚。市场语境里的白马一般指经营稳健、盈利持续、信息披露相对透明、股价波动相对温和的一类公司。它不是一个精确的财务定义更像一组特征的集合赚钱能力强、负债不高、现金流真实、有持续分红能力、业绩不大会大起大落。问题在于这些描述全部是定性的没法直接写成代码。所以要做的第一件事就是翻译。把赚钱能力强翻译成净资产收益率ROE连续多年高于某个水平把负债不高翻译成资产负债率低于某个比例把现金流真实翻译成经营性现金流净额与净利润的比值接近或大于 1把持续分红翻译成分红年限和股息率的组合。翻译完成之后选股就变成了一个和k 0同构的问题若干个数值条件用与运算连起来。我特别想强调一点翻译过程本身就带着取舍没有唯一正确答案。有人更看重 ROE有人更看重现金流有人把分红率放第一位。这些取舍会直接决定你筛出来的池子长什么样。所以不要把某套阈值当成标准答案抄而要理解每个指标在防什么。3.2 五个核心指标与它们各自在防什么下面这张表是我自己常用的一套初筛指标阈值给的是常见参考区间不是绝对标准指标常见参考阈值它在防什么数据来源ROE净资产收益率连续 3 年 ≥ 15%防不赚钱和靠杠杆撑利润年报、季报毛利率≥ 25% 且波动小防没有定价权、靠走量利润表资产负债率≤ 50%非金融类防高杠杆放大风险资产负债表经营现金流/净利润≥ 0.8三年均值防纸面利润、收不回钱现金流量表分红连续性近 5 年均有分红防只融资不回馈分红公告每一行背后都有一堆细节。比如 ROE 要看是不是靠高负债推上去的杜邦拆解一下就知道ROE 净利率 × 总资产周转率 × 权益乘数如果权益乘数特别高那这个 ROE 的含金量就要打折。再比如毛利率单看一年没意义要看三到五年的趋势毛利率持续下滑往往说明竞争格局在恶化。资产负债率这一条要分行业看。制造业 50% 已经偏高但某些重资产行业普遍在 60% 以上银行、地产这类本身靠杠杆经营的行业更是不能套用同一把尺子。这就是为什么我说阈值不能拍脑袋——同一个数字在不同行业里的含义完全不同。经营现金流这一条我放的位置比较高。见过太多账面利润漂亮但应收账款一路飙升的案例利润是真金白银还是白条现金流量表比利润表诚实得多。用经营现金流净额 / 净利润这个比值长期均值在 0.8 以上算健康长期低于 0.5 就要警惕。3.3 阈值怎么定用分位数而不是拍脑袋新手最容易犯的错是直接抄一个ROE 大于 15%就开筛结果发现某一年整个池子只剩三五家另一年又冒出上百家。这不是标准错了是固定绝对阈值在不同市场环境下不稳定。更稳的做法是用分位数在当期全样本里取 ROE 排名前 30% 作为一条线这样无论市场整体盈利水平怎么波动你筛出来的数量大致稳定。具体实现很简单MATLAB 里prctile就能算分位数roeCut prctile(T.ROE, 70); % 取 70 分位即前 30% debtCut prctile(T.DebtRatio, 40); % 负债率取后 40%越低越好 condROE T.ROE roeCut; condDebt T.DebtRatio debtCut;分位数法的好处是自适应坏处是永远能筛出一批哪怕全市场质量都在下滑它也会硬选出前 30%。所以我的做法是两条线并用先用绝对阈值做底线过滤比如 ROE 必须为正、负债率不超过 70%再用分位数做相对排序。这样既保证了基本质量又避免了数量剧烈波动。3.4 硬阈值全通过还是加权打分排序这是整个筛选框架里最重要的一个选择我建议认真想清楚。硬阈值模式就是所有条件必须同时满足pool T(cond1 cond2 cond3 cond4 cond5, :)。优点是逻辑清晰、可解释性强筛出来的每一家你都能说清楚它为什么在池子里。缺点是太脆——只差 0.1 个百分点就被排除而这点差异可能完全在数据噪声范围内。加权打分模式是先给每个指标算一个标准化分数再加权求和排序取前 N 名。优点是稳健单个指标的小幅波动不会导致进出池子缺点是要定权重而权重本身带有主观性并且容易被某一项特别高拉高总分。模式可解释性稳定性参数敏感度适合场景硬阈值全通过高低高池子较小、需要逐家研究加权打分排序中高中池子较大、需要批量管理两级组合高高中我实际采用的方案我实际用的是两级组合第一级用绝对阈值做底线过滤把明显不合格的剔掉第二级对通过底线的那批做加权打分取前若干名。这样既保留了可解释性又不会因为某个指标差一点点就误杀。代码结构上就是先T1 T(condBase, :)再对T1算 score 排序。4. 完整实操从数据表到候选池4.1 数据准备与清洗是最耗时的环节真正动手做过的人都知道写筛选逻辑可能只占两成时间八成时间花在把数据弄成能用的样子。常见的脏数据有几类缺失值某些年份某项指标没披露、口径不一致不同年份的报表科目合并方式变了、追溯调整今年报的去年数据和你去年存的不一样、单位不统一有的用元有的用万元。缺失值我的处理原则是核心指标缺失直接剔除辅助指标缺失用行业中位数填补并打标记。所谓核心指标就是 ROE、资产负债率这几个缺了就没法判断辅助指标比如某个细分费用率缺了可以用同行业中位数补同时加一列isImputed记录哪些是补的后面复核时重点看。追溯调整这个坑特别隐蔽。你用今年下载的数据回测三年前的筛选其实是用后视镜看历史结果会偏乐观。稳妥的做法是在每个时间点存一份当时的数据快照回测时只用快照里的数据。这件事做起来麻烦但如果不做回测结果基本没有参考价值。单位问题用一行代码就能避免大半麻烦把所有比率类指标统一成小数15% 存成 0.15金额类统一成同一量级。我有一次就是因为一半数据是百分数一半是小数导致阈值判断全部失效筛出来的池子完全随机排查了一下午才发现。4.2 一个可以照着改的筛选器实现下面这段代码是一个完整的初筛骨架用虚构的模拟数据演示结构可以直接套到真实数据上function pool screenWhiteHorse(T, opt) % SCREENWHITEHORSE 白马股初筛骨架示例用非投资建议 % T : table每行一家公司列为各项财务指标 % opt : 参数结构体 arguments T table opt.RoeFloor double 0.10 opt.RoeYears double 3 opt.DebtCeil double 0.50 opt.CashRatioFloor double 0.80 opt.DividendYears double 5 opt.TopN double 50 end % ---------- 第一步基础合法性检查 ---------- requiredVars [ROE3Y,DebtRatio,CashRatio,DivYears,GrossMargin]; assert(all(ismember(requiredVars, T.Properties.VariableNames)), ... 缺少必要字段请检查输入表); % 缺失值核心字段缺失直接剔除 valid ~any(isnan(T{:, requiredVars}), 2); T T(valid, :); fprintf(清洗后剩余 %d 家公司\n, height(T)); % ---------- 第二步绝对阈值底线过滤 ---------- condRoe T.ROE3Y opt.RoeFloor; condDebt T.DebtRatio opt.DebtCeil; condCash T.CashRatio opt.CashRatioFloor; condDiv T.DivYears opt.DividendYears; condMargin T.GrossMargin 0.20; baseMask condRoe condDebt condCash condDiv condMargin; T1 T(baseMask, :); fprintf(底线过滤后剩余 %d 家公司\n, height(T1)); if isempty(T1) pool T1; return; end % ---------- 第三步指标标准化 ---------- % 手写 min-max避免依赖额外工具箱 mm (x) (x - min(x)) ./ max(max(x) - min(x), eps); sRoe mm(T1.ROE3Y); sMargin mm(T1.GrossMargin); sCash mm(T1.CashRatio); sDebt 1 - mm(T1.DebtRatio); % 负债率越低越好反向 sDiv mm(T1.DivYears); % ---------- 第四步加权打分 ---------- w [0.30, 0.20, 0.25, 0.15, 0.10]; score w(1)*sRoe w(2)*sMargin w(3)*sCash ... w(4)*sDebt w(5)*sDiv; T1.Score score; % ---------- 第五步排序取前 N ---------- T1 sortrows(T1, Score, descend); n min(opt.TopN, height(T1)); pool T1(1:n, :); fprintf(最终候选池 %d 家最高分 %.3f最低分 %.3f\n, ... height(pool), pool.Score(1), pool.Score(end)); end这段代码里有几个设计决定值得展开。arguments块用来做参数校验和默认值比在函数体里写一堆if nargin 2清爽得多新版本 MATLAB 都支持。缺失值处理放在最前面因为后面所有比较都假设数据是干净的如果先算条件再剔除 NaN逻辑上会出现NaN 参与比较返回 false看起来像不通过的隐式行为容易误判。标准化那段我手写了 min-max 而没用normalize原因是normalize属于统计工具箱很多环境里没装。手写版本有个细节分母加了eps防止所有值相同时除零。这个eps其实就是前面讲的浮点容差思路的又一次应用同一个思维在不同地方反复出现。负债率做了反向处理1 - mm(...)因为越低越好的指标不能直接用原始值加分否则高负债反而得分高。这种方向性错误是打分模型里最常见的 bug写完一定要用极端值验证一遍把某家的负债率手动改成 0.9看它分数是不是明显下降。4.3 结果复核代码筛完不等于研究结束筛出来的名单只是候选不是结论。我会做几件事做人工复核。第一看是不是集中在某一个行业——如果 50 家里 40 家都是同一个行业那说明你的阈值实际上在筛行业不是在筛公司质量这时候要分行业做标准化。第二看分数分布是不是断层——如果第 1 名 0.95、第 2 名 0.62中间空一大截那第 2 名之后的参考价值就要打问号。第三逐个检查极端值某家 ROE 高达 80%先别高兴大概率是净资产很小导致的比率失真或者是一次性收益。还有一件事必须做看指标的时间序列不只看当期值。ROE 连续五年稳定在 16% 左右和一年 30% 其余几年 8% 波动完全是两种公司。我一般会额外拉一个三年趋势图肉眼扫一遍异常波动的挑出来单独看原因。这一步代码不好完全自动化但可以用简单的方差过滤先筛一遍三年 ROE 标准差超过某个值的标个警示标记。4.4 再平衡与记录让筛选变成可持续的流程筛选不是一次性动作。市场在变财报在更新你的池子也应该定期刷新。我一般按季度做一次财报季结束后跑一遍脚本和上期结果做对比看进出情况。进出本身不是问题问题是每次进出都要能说出原因是基本面变了还是数据口径变了还是阈值调整了。所以我建议在表里加三列screenDate、screenVersion、reasonTag。screenVersion记录当时用的参数组合阈值、权重reasonTag记录这次进出池的主要原因分类。做上一年你就能看出自己的参数在什么市场环境下会失灵这比任何理论推导都有价值。这套做法的思路和代码里给函数加版本号、给日志加 trace id 是一回事都是为了让当时为什么这么做这件事可追溯。5. 常见问题排查速查5.1 MATLAB 侧的高频坑现象可能原因排查方法if k 0报条件必须为标量k 是数组改用all(k 0)边界值 0 被判为负数浮点误差实际值 -1e-16引入 tol用k -tol结果里混进 NaN 行用了~(k0)写法改正向判断或先剔除 NaNuint8相减永远非负无符号整数下溢截断先转 double 再运算复数比较结果诡异只比较实部显式检查虚部或取 real逻辑索引后维度不符索引结果恒为列向量用reshape或转置整数类型比较报错两侧类型不一致统一转 double 或同类型表里第二条我再补充一句容差不要随手写 1e-6 了事最好根据数据量级用eps推。有一个简单的经验公式tol 10 * eps(norm(k))对于量级在 1e3 左右的数据这个值大概是 1e-12 量级足够吸收累积误差又不会误放行。5.2 选股侧的高频坑现象可能原因处理方式某年筛选结果只剩几家绝对阈值在弱市环境下过严改用分位数或加绝对底线后放宽池子高度集中在单一行业未做行业中性化分行业内分别排序再合并回测结果异常漂亮用了追溯调整后的数据用当期快照数据重跑某家指标好得离谱分母极小或一次性收益检查净资产规模与非经常性损益分数相近但排名差距大权重过于集中检查权重敏感性做扰动测试进出池频繁阈值卡在分布密集区引入缓冲带进出用不同阈值最后一条缓冲带值得单独说。如果你的筛选线正好落在样本分布最密集的地方那一点微小的数据变动就会导致大量进出管理成本极高。解决办法是设两条线进池要求分数 ≥ 0.70出池要求分数 ≤ 0.60中间的维持原状。这个技巧在工程里叫迟滞hysteresis用在这里能显著降低换手。5.3 我在实操中总结的几条经验第一条先跑通再调参。很多人一开始就在阈值上纠结半天其实应该先用一组宽松参数把整个流程跑通看看输出长什么样再回头调。流程不通的时候调参是盲目的。第二条把你的条件写成人能读懂的一句话。如果某个筛选规则你没法用一句话说清楚它在防什么那这条规则大概率是多余的。我删过好几条看起来专业但其实没想清楚的指标删完之后结果反而更稳定。第三条代码里的浮点容差思维要带到数据里。财务数据本身就有精度和口径的不确定性用 15.0% 卡 ROE 和用 14.8% 卡在统计上没区别但在结果上可能差出几十家。所以阈值附近一定要留缓冲或者在报告里标注临界样本。第四条也是我觉得最重要的一条筛选器只能帮你缩小范围不能替你做判断。它做的是把你从看几千家变成看几十家剩下的工作还是得靠读年报、看行业、理解生意。工具的价值在于节省机械劳动不在于替代思考。这一点在写代码和做筛选上是完全一致的——k 0只能告诉你这个数是不是非负它没法告诉你这个数为什么是这个值。提示文中涉及的财务指标阈值、权重设置均为方法论演示不构成任何投资建议。任何筛选模型都存在失效的可能实际使用前请自行验证并做好风险控制。最后分享一个小习惯。我在写任何筛选脚本的时候都会在文件头留一段注释写清楚三件事这套条件在防什么、参数是什么时候定的、上一次修改是因为发现了什么问题。这段注释比代码本身更值钱因为半年后你回头看能想起来的往往只有这段注释。写代码如此建筛选池也如此。