合规说明:本文章为个人观点,仅供用户学习参考使用,切勿当作你的决策依据。文中提到的任何个股或思路,均不构成投资建议。假设你是量化用户,该量化思想仅供你个人使用,切勿传播。

    前言:本文章使用掘金量化终端进行交易回测。后续附带的部分量化操作源码为C# 代码,掘金支持C#、Python、C++、Matlab进行量化开发,知道思路以后,客官可以自行改造成你熟悉的语言进行开发尝试。如果你用的是QMT客户端,同样也可以自行更换数据源和获取数据并做处理,然后按照类似思路开发你自己的代码。当然,如果你没有量化账号进行操作,本文的首板战法、一进二,同样值得你当作参考。

    备注:本文的首板战法,特指打板。前面有一篇文章,写的首板挖掘,这个注重埋伏,是两个完全不同的思想体系。

    下图是我在一个之前创建的量化群里面做了个简单的文字直播,测试回测效果的。5W到42W,2024年五月中旬到2025年年初的量化回测战绩,实际上并没有一年。那时候没做文本记录,所以下面提供的详细买卖点记录,是选了个更早的,2023年5月中旬开始,到12月初的,半年不到,5W到20W,选择这个区间是因为大A长期是处于这样一个震荡行情,所以效果明显一点,并且会选择其中的个别特殊案例进行实盘解说。

    本量化操作的一个初始化配置如下:

1、初始设置本金:5W
2、仓位控制:最多持仓3只票,持仓数量根据资金变化情况自动计算。简单计算规则:买入手数=(int)资金总量/3/100/11 
3、打板时候,挂单计算当前排单量,每次分笔数据进行减掉成交量;   如果小于0,代表可以买进,同时记录B点;   如果到尾盘,统计的成交量都没有前面挂单时候的封板量多,   说明当天排单失败,撤销挂单。
4、卖出时候,如果当前是跌停板,则不进行卖出,模拟跌停板出不去的情况。
5、选股只选20元以内的个股.如果第二天打板,买入价格有可能超过20元。
6、选股特殊规则:文章下方付费内容进行详细解说。毕竟精彩部分,不能白白送人。
7、每天定时8点20进行选股,符合第六条特殊条件的个股,加入监控池。  监控池内的个股,当天开盘会实时收到tick分笔数据推送过来。  根据tick数据进行判断上板先后,只打第一个上板的票;  挂单委托成功以后,取消订阅其他监控池内的股票。
8、卖出规则,当天涨幅低于前一天收盘价2%,直接止盈止损。   如果大于2%,尾盘下午14点25左右没有涨停,同样进行止盈止损。 9、交易佣金,预设万分之1.2,这个是大部分人的一个可能的佣金率。我本人是万分之0.8   其他费用:如果交易佣金不足5元,按照5元扣除   印花税:卖出计算印花税,按照千分之一扣除   过户费等:占比太低,此处忽略不计了。

    tick分笔数据监听与打板规则源码解析:

    掘金OnTick函数可以用于监听分笔数据,也就是监控哪些个股,它会实时对分笔数据进行推送。监听分笔数据,普通用户5档;L2用户可以监听10档。L2有些券商只对机构用户,或者资金500W以上用户提供,专业投资者可以尝试;普通投资者机器和网络不一定承接得住L2数据(还包括实时逐笔数据)

    9点24之前不操作,已经有3只持仓也不进行打板。

public override void OnTick(Tick tick)   {       if ((tick.createdAt.Hour <= 9 && tick.createdAt.Minute < 24))       {           // 集合竞价未结束不打           return;       }
       if (!hasBuyList.Any(x => x.symbol == tick.symbol) && !hasBuyList.Any(x=>x.time.Date==tick.createdAt.Date))        {           // 打板           if (hasBuyList.Count() >= 3)           {               // 当前持仓大于等于3只,不打               return;           }

        9点24分50秒起,符合一字板条件的,准备打板。打板需要额外判断是否符合特殊条件。该条件是首板打板的关键。如果符合,就会加入已购集合内,并且状态为排单状态 state=2;这个集合是自定义的类。

if (tick.createdAt.Hour == 9 && tick.createdAt.Minute == 24 && tick.createdAt.Second > 50)  {      // 一字打板      if (tick.quotes[0].askVolume == tick.quotes[0].bidVolume &&  tick.quotes[1].askVolume == 0 && tick.quotes[1].bidVolume * tick.price > 10000000)      {          if (_historySymbol.symbol.Any(x => x.Key == tick.symbol))          {              // 特殊取值操作,此处不做展示              if (特殊条件,此处不做展示)              {                  // 计算所需购入的手续费                  var buyMoney = buyNumber * tick.quotes[0].bidPrice * 100;                  var handPrice = buyMoney * 0.00012;                  if (handPrice < 5)                  {                      handPrice = 5; // 不足5元按照5元计算手续费                  }
                  if (buyMoney + handPrice > totalMoney)                  {                      return; // 现金余额不足,不打                  }                  hasBuyList.Add(new HasBuyState()                  {                      hands = buyNumber,                      price = tick.quotes[0].bidPrice,                      symbol = tick.symbol,                      time = tick.createdAt.Date,                      lastClose = tick.price,                      bigMoney = tick.quotes[1].bidVolume,                       limitVol = tick.quotes[1].bidVolume,                      state = 2,                      realVolState = 1                  });                  AnsiConsole.MarkupLine($"【{tick.createdAt}】:委托挂单排板: {tick.symbol}  价格:{tick.price:F2} ");                  Log.Information($"【{tick.createdAt}】:委托挂单排板:股票代码: {tick.symbol}  挂单价格:{tick.price:F2}  挂单手数:{buyNumber}");                  string[] sbs = _historySymbol.symbol.Keys.ToArray(); // 当天监听的所有个股列表                  foreach (string s in sbs)                  {                      if (!hasBuyList.Any(x => x.symbol == s))                      {                          Unsubscribe(s, "tick"); // 不在已购列表的,进行退订                      }                  }              }
          }      }      return;  }

已购持仓实体类定义如下:

    尾盘不进行打板操作,因为尾盘偷鸡上板的,大多数都是坑,宁可错过也不要买。尾盘定义:下午两点半开始。并且集合竞价以后。25分到30分之间,不进行处理,这部分可以挂单,但是不会成交,所以不会有分笔成交出现,直接过滤。

    除了以上特殊时间外,其他正常交易时间,如果涨停,则直接挂单。挂单以后,对其他监听的未挂单个股进行退订,这样就不会继续收到其他个股分笔数据了,因为一天只买一只的目的达成了。

    如果已经购买,但是还没成交的,在排单期间,后续的每次分笔推送,都会进行判断成交量,封单量累计减掉成交量直到小于0,就代表你打板买进去了。

    判断出货,详情可见下方源码解释:

else if (hasBuyList.Any(x => x.symbol == tick.symbol && x.state == 0 && tick.createdAt.Date != x.time.Date)) {     // 次日,判断是否卖出     var buyed = hasBuyList.Where(x => x.symbol == tick.symbol).First();     if (tick.createdAt.Hour <= 9 && tick.createdAt.Minute < 30)     {         return;  // 竞价不做判断     }     if ((tick.createdAt.Hour == 9 && tick.createdAt.Minute >= 30) || (tick.createdAt.Hour > 9 && tick.createdAt.Hour < 15))     {         if (tick.quotes[0].bidVolume == 0)         {             // 跌停 无法出货             return;         }         // 如果低于1%          if ((tick.price - buyed.lastClose) / buyed.lastClose < 0.01 && (tick.createdAt.Hour < 14 || (tick.createdAt.Hour == 14 && tick.createdAt.Minute <= 25)))         {             // 计算出货手续费             var saleMoney = hasBuyList.FirstOrDefault(x => x.symbol == tick.symbol).hands * 100 * tick.quotes[0].bidPrice;             var handPrice = saleMoney * 0.00012;             if (handPrice < 5)             {                 handPrice = 5;             }             handPrice += saleMoney * 0.001; // 加上印花税
             GMData<Order> o = OrderValue(tick.symbol, saleMoney, OrderSide.OrderSide_Sell, OrderType.OrderType_Market, PositionEffect.PositionEffect_Close, tick.price, "量化账号ID");             if (o.status == 0)             {                 totalMoney = totalMoney + saleMoney - handPrice;
                 AnsiConsole.MarkupLine($"【{tick.createdAt}】:S   {tick.symbol}  Price:{tick.quotes[0].bidPrice:F2} ,手续费:{handPrice:F2}  盈亏:{saleMoney - buyed.price * buyed.hands * 100 - handPrice:F2}   现金余额:{totalMoney:F2} ");
                 Log.Information($"【{tick.createdAt}】:S 委托成交卖出:股票代码: {tick.symbol}  成交价格:{tick.quotes[0].bidPrice:F2}  手数:{buyed.hands}  手续费:{handPrice:F2}  盈亏金额:{saleMoney - buyed.price * buyed.hands * 100 - handPrice:F2}   现金可用余额:{totalMoney:F2}");
                 hasBuyList.Remove(buyed);                 Unsubscribe(tick.symbol, "tick");                 return;             }             else             {                 Console.WriteLine("S 失败");             }         }

尾盘出货:

// 尾盘判断,如果不是涨停板,清仓  else if (tick.createdAt.Hour == 14 && tick.createdAt.Minute > 25 && tick.quotes[0].askVolume > 0 && tick.quotes[1].askVolume > 0)  {      // 计算出货手续费      var saleMoney = hasBuyList.FirstOrDefault(x => x.symbol == tick.symbol).hands * 100 * tick.quotes[0].bidPrice;      var handPrice = saleMoney * 0.00012;      if (handPrice < 5)      {          handPrice = 5;      }      handPrice += saleMoney * 0.001; // 加上印花税

      GMData<Order> o = OrderValue(tick.symbol, saleMoney, OrderSide.OrderSide_Sell, OrderType.OrderType_Market, PositionEffect.PositionEffect_Close, tick.price, "你自己的账号ID");      if (o.status == 0)      {          totalMoney = totalMoney + saleMoney - handPrice;          AnsiConsole.MarkupLine($"【{tick.createdAt}】:S   {tick.symbol}  Price:{tick.quotes[0].bidPrice:F2} ,手续费:{handPrice:F2}   盈亏:{saleMoney - buyed.price * buyed.hands * 100 - handPrice:F2}   现金余额:{totalMoney:F2} ");
          Log.Information($"【{tick.createdAt}】:S 委托成交卖出:股票代码: {tick.symbol}  成交价格:{tick.quotes[0].bidPrice:F2}  手数:{buyed.hands}  手续费:{handPrice:F2}  盈亏金额:{saleMoney - buyed.price * buyed.hands * 100 - handPrice:F2}   现金可用余额:{totalMoney:F2}");
          hasBuyList.Remove(buyed);          Unsubscribe(tick.symbol, "tick");          return;      }      else      {          Console.WriteLine("S 失败");      }  }

    尾盘最后一笔分笔数据推送过来进行接收,也代表当天结束了。如果这个时候排单没进去,当天打板就属于没买到。

if (tick.createdAt.Hour == 15 && tick.createdAt.Minute == 0)  {      var buyed = hasBuyList.FirstOrDefault(x => x.symbol == tick.symbol);      if (buyed.state == 2)      {          buyed.limitVol -= tick.lastVolume; // 排单量递减          if (buyed.limitVol < 0)          {              // 尾盘竞价买入成功              // 排单成功,计算费用              // 计算所需购入的手续费              var buyMoney = buyNumber * tick.price * 100;              var handPrice = buyMoney * 0.00012;              if (handPrice < 5)              {                  handPrice = 5; // 不足5元按照5元计算手续费              }

              GMData<Order> o = OrderValue(tick.symbol, buyMoney, OrderSide.OrderSide_Buy, OrderType.OrderType_Market, PositionEffect.PositionEffect_Open, tick.price, "你自己的账号ID");              if (o.status == 0)   //该判断仅表示函数调用无异常              {                  totalMoney = totalMoney - buyMoney - handPrice; // 现金余额
                  AnsiConsole.MarkupLine($"【{tick.createdAt}】:B   {tick.symbol}  Price:{tick.price:F2}  手续费:{handPrice:F2}  现金余额:{totalMoney:F2}");                  Log.Information($"【{tick.createdAt}】:B 委托成交买入:股票代码: {tick.symbol}  成交价格:{tick.price:F2}  手数:{buyed.hands}  手续费:{handPrice:F2}  现金可用余额:{totalMoney:F2}");              }              buyed.state = 0;              buyed.lastClose = tick.price;          }          else          {              hasBuyList.Remove(buyed);              AnsiConsole.MarkupLine($"【{tick.createdAt}】:委托挂单排板失败!截止收盘未排到单: {tick.symbol}  ");              Log.Information($"【{tick.createdAt}】: 委托挂单排板失败!股票代码: {tick.symbol}  手数:{buyed.hands}  挂单废单,撤销");          }      }      else      {          buyed.lastClose = tick.price;      }  }

    以下是2023年5月份到2023年11月份的所有量化回测的挂单、BS点日志记录,也是本战法的买卖点(文章后面部分有完整版记录)。5W到19.9W,接近翻3倍。该战法正常情况下,可能收益不明显,处于震荡期;但是只要抓到一个,就可以让资金快速滚动。例如,以下是记录的其中一条,前面历时3个月才从5W震荡到6W多,打到一个捷荣,几天就上升到8W多。买点是2023年8月30直接排板,出货日按照前面的代码解说,就会在9月7号尾盘没涨停出货,11块吃到19块。

    这个相当于是妖股了,后面又走了接力抱团,但是在本量化策略体系中,或者首板战法上看,就不是买点了。量化吃连扳区间如下图所示:

    上面是其中一个买点,首板/一进二的买点,有比较高的要求,不是所有板都能打。如果上面的买点,各位客官能够看出端倪,接下来的付费内容可以不用解锁。如果确实想知道特殊条件的,也欢迎解锁,当作是请作者的一顿饭钱了,毕竟分享写这么多字确实不容易。

    如果你是在知乎、CSDN等其他外链平台上面看到我的文章,可能找不到原文。要寻找原文,可以通过VX公众号进行寻找。本人公众号:Dotnet Dancer    个人VX号:WeskyNet001

首板战法技术详解:

    这是一份2023年5月11日开始到2023年11月29日,5W本金滚动到19W9的所有操作交易记录:

Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐