hxj20061987
V2EX  ›  股票

6 步选股 4 个坑 5 年 14 倍:"菜场大妈"策略拆解优化实录

  •  
  •   hxj20061987 · 5 days ago · 553 views

    一、开篇:我,被大妈教育了

    最近在聚宽社区翻到一个叫"菜场大妈"的策略,名字接地气,回测成绩还挺能打。今天来拆解一下它的核心逻辑,顺便做点优化。

    花了不少时间研究量化,看文章、调参数、研究各种多因子模型,每天盯着 Alpha 信号、因子暴露、信息比率……然后呢?

    年化跑不赢沪深 300 。

    有一天我妈跟我说,她闺蜜在菜场摆摊,顺手买了几只"便宜的、但公司还在赚钱的"小股票放着,几年下来收益还挺好。

    我当场沉默了三分钟。


    大妈炒股的逻辑,其实就三条:

    1. 便宜——贵的我买不起,也不放心
    2. 有肉——不能是只讲故事的空壳子,得有真收入
    3. ——大公司牛人多,哪轮到我捡漏

    听起来很土?但这三条,其实就是"小市值低价策略"的核心逻辑。


    二、大妈的选菜秘籍

    整个策略的核心思路,可以用一套"去菜场买菜"来解释。

    第一步:扔掉烂菜叶子(基础过滤)

    大妈第一眼看菜,先排除烂掉的、发霉的、一看就没法吃的

    股票里的"烂菜叶子"就是:

    • ST 股票:名字前面贴了黄色标签,绝对不碰,看着便宜,全是坑
    • 退市边缘股:名字里带"退"的,大妈直接转身走人
    • 停牌股:菜摊今天关门,你掏钱也买不到
    # 过滤 ST 及其他具有退市标签的股票
    def filter_st_stock(stock_list):
        current_data = get_current_data()
        return [stock for stock in stock_list
                if not current_data[stock].is_st           # 不是 ST
                and 'ST' not in current_data[stock].name   # 名字里没有 ST
                and '*' not in current_data[stock].name    # 没有星号(*ST )
                and '退' not in current_data[stock].name]  # 没有退字
    

    另外,科创板和北交所也不去——大妈不去高档精品超市,她就逛老菜市场,接地气,看得懂,买得放心。

    # 过滤科创北交股票
    def filter_kcbj_stock(stock_list):
        for stock in stock_list[:]:
            if stock[0] == '4' or stock[0] == '8' or stock[:2] == '68' \
               or stock[:3] == '300' or stock[:3] == '301':
                stock_list.remove(stock)
        return stock_list
    

    第二步:检查有没有肉(基本面防雷)

    光便宜不行,还得有真材实料

    大妈挑肉的标准很简单:

    • 净利润必须大于 0——不赚钱的公司,买它干什么?
    • 营业收入大于 1 亿——连 1 个"小目标"都没有,别浪费咱的买菜钱
    q = query(
        valuation.code,
        valuation.market_cap,
        income.np_parent_company_owners,  # 归母净利润
        income.net_profit,                # 净利润
        income.operating_revenue          # 营业收入
    ).filter(
        valuation.code.in_(stocks),
        valuation.market_cap.between(g.min_mv, g.max_mv),   # 市值区间
        income.np_parent_company_owners > 0,                 # 有真肉
        income.net_profit > 0,                               # 净利润也得正
        income.operating_revenue > 1e8                       # 营收过亿才算数
    ).order_by(valuation.market_cap.asc())                   # 市值从小到大排
    

    这个筛选直接干掉了绝大多数"讲故事的仙股"。


    第三步:大妈的极简审美——10 块以下才考虑

    这是大妈最硬核的原则:

    超过 10 块钱的菜,太金贵,不买。

    # 过滤股价高于 10 元的股票
    def filter_highprice_stock(context, stock_list):
        last_prices = history(1, unit='1m', field='close', security_list=stock_list)
        return [stock for stock in stock_list
                if stock in context.portfolio.positions.keys()
                or last_prices[stock][-1] <10]
    

    原版策略过滤的是 9 元以上,我稍微放宽到 10 元——毕竟通货膨胀嘛,大妈也得跟上时代。


    第四步:专挑边角料——市值最小的 4 只

    经过前面三轮筛选,剩下的候选菜已经都是"便宜有肉的好货"了。

    接下来怎么选?按市值从小到大排,挑最"边角料"的 4 只。

    捡漏心理:专挑摊位最边角没人注意的那几样,够小、够便宜、胜在没人哄抬价格。

    目标市值区间:10 亿到 100 亿之间(不能太小,太小容易跑路;不能太大,太大轮不到散户吃肉)。

    g.stock_num = 4       # 最多买 4 只
    g.min_mv = 10         # 最小市值 10 亿
    g.max_mv = 1e8        # 最大市值 1000 亿(写法是万亿单位,实为 100 亿)
    

    第五步:隔夜馊了,赶紧处理(卖出逻辑)

    大妈有一条铁律:隔夜的菜不留。

    这里对应的是"昨日涨停股"的处理:

    这只股票昨天表现亮眼,涨停了!大妈开心,夸它新鲜。 今天一看,没连板,价格缩回来了。 大妈立刻:**"隔夜的,赶紧处理,换新鲜的来。"**

    def check_limit_up(context):
        current_data = get_current_data()
        if g.high_limit_list:
            for stock in g.high_limit_list:         # 昨天涨停的票
                if current_data[stock].last_price \
                   <current_data[stock].high_limit:  # 今天没继续涨停
                    order_target(stock, 0)            # 清仓
                    g.just_sold.append(stock)         # 记录已卖,不二次买入
        # 卖了之后,如果持仓不够,再候补买入
        position_count = len(context.portfolio.positions)
        if g.stock_num > position_count and position_count != 0:
            my_Trader(context)
            psize = context.portfolio.available_cash / (g.stock_num - position_count)
            for s in g.choice:
                if s not in context.portfolio.positions and s not in g.just_sold:
                    order_value(s, psize)
                    if len(context.portfolio.positions) == g.stock_num:
                        break
    

    注意先卖后买,这是个细节优化——原版是先买后卖,导致卖掉的票要隔天才能补仓,白白空仓一天。


    第六步:一周去一次菜场(换仓节奏)

    大妈不每天去菜场,那太累了。她每周一早上进一次城,买够就回家

    run_weekly(my_Trader, 1, time='13:50')  # 每周第 1 个交易日,13:50 选股
    run_weekly(go_Trader, 1, time='14:00')  # 每周第 1 个交易日,14:00 下单
    

    每周换一次仓,频率不算高,也省手续费。大妈的核心竞争力之一,就是不天天折腾


    三、翻车现场实录——代码比菜还难伺候

    写策略的过程,踩了不少坑,挑几个有代表性的跟大家分享。

    坑一:滑点是隐形摊位费

    刚开始没设滑点,回测数据漂亮得一塌糊涂。

    等我加上 FixedSlippage(0.02) 和真实手续费之后,收益率肉眼可见地往下掉。

    set_slippage(FixedSlippage(0.02))
    set_order_cost(OrderCost(
        close_tax=0.001,            # 印花税 0.1%(卖出才收)
        open_commission=0.0001,     # 买入佣金 0.01%
        close_commission=0.0005,    # 卖出佣金 0.05%
        min_commission=0.1          # 最低 5 毛
    ), type='stock')
    

    这就是菜场的"摊位费":你看着菜很便宜,结果各种税费加起来,利润被切走一大块。

    结论:纸面富贵不算数,扣完成本才是真收益。


    坑二:提前钦定几只 ETF ,曲线漂亮到怀疑人生——这叫上帝视角,不叫量化

    这个坑不是代码 bug ,是人的 bug 。

    接手这个策略的时候,它已经配了一段"组合配置"的逻辑——在小市值股票之外,还额外指定了几只 ETF:

    黄金 ETF( 518880 )、纳斯达克 ETF( 513100 )、芯片 ETF( 159995 )……

    配置理由写得头头是道:分散化、降回撤、对冲 A 股风险……

    一跑回测:2016 年到 2025 年,年化亮瞎眼,曲线好看得像 PPT 配图。

    我当时觉得自己是天才。

    然后仔细一看,冷汗下来了——

    这几只 ETF 是谁选的?按什么选的?

    黄金,2024 年大涨;纳斯达克,2023 年翻倍反弹;芯片,也有过自己的高光时刻……

    这些都是已经发生的历史。策略里写死了"就买这几只",然后用历史回测来"验证"它们表现好——

    这不是量化,这是开卷考试然后说自己考满分


    把这几只硬编码的 ETF 全部剔除之后,换成动态筛选的逻辑——

    收益率啪啪往下掉。

    曲线一下子瘦了一大圈,以前那段"漂亮区间"直接垮掉一半。

    这就是过拟合的最朴素版本:用上帝视角挑菜,当然买的全是好菜。但你在真实菜场里没有上帝视角,前一天猪肉涨价了你也不知道。

    结论:回测数据越漂亮,越要多问一句"为什么"——是策略真的好,还是你亲手喂了它正确答案?


    坑三:9:30 下单,大妈两手空空

    这个坑藏得很深,或者说——回测系统藏得太好了

    策略里有一段逻辑,要在 9:30 开盘第一分钟执行买卖:

    run_daily(check_limit_up, time='9:30')
    

    理论上完全合理——越早下单越好,抢先手嘛。

    回测跑下来,成交记录里也都有,数据漂漂亮亮。

    但如果一上 SHIPAN ,就尴尬了。

    9:30 开盘的第一分钟,实际上是集合竞价刚刚结束的瞬间。行情数据还没稳定下发,API 还没完全就绪,大量股票这一秒钟根本拿不到有效的实时价格——

    于是下单,要么直接报错,要么以 0 价格挂出去被拒单,总之:两手空空,什么都没买到。

    回测里的 9:30 是"模拟的 9:30",数据是现成的,下单当然成功。 ******里的 9:30 是"真实的 9:30",数据还在飞,根本接不住。


    改成 9:319:35 之后,下单终于正常了。

    但收益往下掉了。

    因为策略里有依赖"开盘价"的买卖判断,哪怕晚了 1~5 分钟,成交价就不一样了,有些单子该买的没买上,该卖的滑了点。

    看起来只差几分钟,代入收益一算,差距比想象中大。

    结论:回测的时间刻度是理想化的,****的第一分钟是混沌的。9:30 下单,在这个系统里就是一个幻觉。**


    坑四:大妈不止损,一套套到认命

    这是原版策略最让我不安的一个设计——它没有止损。

    大妈的哲学是:买来的菜,就算有点蔫,也不扔。泡泡水,明天还能吃。

    策略里只有两种卖出情形:

    1. 股票掉出候选池(市值涨太大、财务变差、变 ST 了)
    2. 昨天涨停今天没连板

    跌了 10%?不管。跌了 20%?继续拿着。只要它还符合小市值条件,就一直持有。

    极端行情一来,这个策略会被套得很难看。2020 年春节后复市那天,我看着日志:

    [止损] 002112 三变科技 成本=6.94 现价=5.83 亏损=-16.0%,强制清仓
    [止损] 600099 林海股份 成本=6.74 现价=5.65 亏损=-16.2%,强制清仓
    [止损] 600493 凤竹纺织 成本=5.67 现价=4.77 亏损=-15.9%,强制清仓
    

    等等,这是我加了止损之后的日志,亏损还是达到了 15%+。

    原因很简单:跌停股票卖不掉。

    止损单挂出去,市场全是卖盘没有买盘,当天根本成交不了。第二天继续跌停,继续挂单,继续成交不了—— 这就是"跳空"的残酷现实:止损线写的是 8%,真正止损的时候可能已经亏了 15%。

    最后加了两个补丁才勉强解决:

    • g.stop_loss_set 记录已发止损单的股票,跌停时静默重试,不重复打日志
    • 止损后的股票本周不再买回,防止刚割肉就被自动补仓
    g.stop_loss_ratio = 0.08    # 止损线 8%
    g.stop_loss_set = set()     # 已发止损单,跌停未成交则次日重试
    

    结论:止损写进代码只是第一步,跌停穿越才是真正的硬伤。极端行情面前,8%的止损线可能保不住你,但有和没有,差距还是很大。


    四、最后说一句

    折腾一圈之后,我发现我最初看不上的东西,反而是最难做到的。

    量化圈有个通病:喜欢追求复杂。因子越多越好,模型越深越好,参数越精细越好。

    但菜场大妈的策略只有三个筛选条件,逻辑三句话说完,参数五个以内,任何人看懂之后都能在脑子里复现一遍。

    这就是它最宝贵的地方:逻辑清晰。

    你知道它为什么买,你知道它为什么卖,你知道它会在什么情况下亏钱,你不会因为"这个信号我也说不清楚为什么"而在极端行情裂开。


    说到稳定性,我的实际体感是这样的:

    • 不会让你一夜暴富——4 只小市值股票,赶上行情好也就是稳稳地跟上指数
    • 也不容易把你打趴下——每周换仓、市值过滤、加了止损,大的单边亏损会被强制截断
    • 回撤控制不完美,极端行情(比如 2020 年春节复市)照样被打,但不会持续失血

    这种感觉就像买的不是最贵的全熟牛排,而是一碗好熬的老火靓汤——慢,但真实,喝完不反胃。


    当然它有很多问题没解决,还需要继续努力:

    • 没有趋势判断,熊市里也一样买买买
    • 止损是硬止损,碰到跌停排队卖不掉的情况还是会穿越
    • 换仓频率固定,遇到消息面剧变反应慢

    回测这么漂亮的策略,拿到全市场里到底能排第几?

    我把这个大妈策略丢进了一个叫 9db 智能体交易竞技场 的地方,那里可以上传交割单,跟别人的策略一起按实时收益 PK 排名,跑了才发现,大佬们的有多稳


    如果这个策略对你有帮助,点个赞就行。如果你发现了什么 Bug 或者有更好的改法,欢迎评论区指教——毕竟,大妈选菜也需要老街坊互相提醒。

    2 replies    2026-06-16 20:20:08 +08:00
    deepseacat
        1
    deepseacat  
       5 days ago
    分析的挺好,9db 看起来是个跟单的
    UxCZbWShjEsL
        2
    UxCZbWShjEsL  
       4 days ago
    感谢分享
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   941 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 64ms · UTC 19:01 · PVG 03:01 · LAX 12:01 · JFK 15:01
    ♥ Do have faith in what you're doing.