2023-02-21|閱讀時間 ‧ 約 23 分鐘

如何第一次量化交易就賠錢

如何第一次量化交易投資就賠錢
身為會寫一點程式碼的工程師,量化交易投資無疑是最具吸引力的投資方式。試想只要寫個小程式定期去追蹤股市大盤交易指數,然後自動下單買入賣出,就會賺進源源不絕的財富,光用想的口水都快流下來了。
坐而言不如起而行。這邊有幾個小問題需要克服:
  1. 即時報價
  2. 下單買賣
  3. 交易策略
市面上有許多現成軟體都可以做到上述功能,例如TradingView、MultiCharts或是XQ,但是都需要繳交不少的年費。還好有些券商願意提供報價與下單的API,例如群益與永豐,只要開戶成功,就可以自行DIY寫程式免費去使用這些API達成前兩項功能。筆者自己是採用群益API,主要是因為API文件解說詳細,而且可以在網路上找到不少範例,開發相對較為快速。
接下來重點就是交易策略了。依據筆者個人初淺的調查研究,交易策略可以分成順勢交易與逆勢交易。順勢交易主要判斷方式就是找出一個震盪區間,一旦往上或是往下突破這個震盪區間,就立刻開始買多或是賣空。逆勢交易就是傳說中的摸頭猜底,網路上許多神人可以透過各種波浪理論準確預言價格高低點,可惜大多採用形態學劃線的方式,筆者功力淺薄,還想不出方式將形態學改寫成為程式邏輯。所以本文會採用順勢交易策略,透過一些指標計算當作範例。交易週期也是交易策略中重要的一環,既然有了電腦無時無刻幫忙監控,當然可以選擇較短的交易週期,筆者瀏覽許多網路文章之後,就先選定台指期5分K當作目標。目前券商提供的API都有C#以及Python版本,因為筆者本身對C語言比較熟悉,所以就選了C#當作實作語言。
程式框架很簡單,分成兩個主要的部份: 1. 讀取券商即時報價tick,並立刻轉換成為1分K或是5分K的資料,以及 2. 每5分鐘讀取一次5分K的資料,帶入指標計算,判斷目前是否為合適的買賣點,如果是的話,執行下單的動作。在開發的過程中,筆者自行摸索許多網路資源,趁著還有些力氣將之記錄下來。
  • 讀取券商即時報價,群益金融網 - 證券API提供註冊個股報價功能,並透過Notify之類的回呼函式取得即時報價。例如筆者想取得的個股報價為台指期,因此註冊的個股代號為「TX00」,若是小台指則為「MTX00」,其中00表示當月報價,如果有指定特別月份報價,則修改00為該月份數字。由於某些時候瞬間的台指期成交筆數非常多,所以儲存資料時建議採用ConcurrentQueue的方式,否則資料很容易就遺失。
  • 指標計算部分,筆者藉由Stock Indicators for .NET函式庫幫忙,以節省自行開發的時間。Skender的指標函式庫涵蓋了許多常見指標,並且持續在github提供更新,對於研究交易指標的原理,不失為良好的研究來源。
  • 即時Tick資料轉OHLC的K棒資料格式,筆者參考[利用 LINQ 實現期指Tick檔彙整為K棒]再稍微改寫符合自己的需求如下,這段 LINQ程式寫得非常高明,筆者感悟一陣子才了解。
        private IEnumerable<OHLCBar> generate_OHLCBar(IEnumerable<Tick> ticks, long barSizeInTicks)
        {
            TimeSpan natural_time = new TimeSpan(8, 45, 0);

            var bars = from tick in ticks
                       let barIndexForDay = Math.Floor(tick.Timestamp.TimeOfDay.Subtract(natural_time).TotalSeconds / barSizeInTicks)
                       let barBeginDateTime = tick.Timestamp.Date.Add(natural_time).AddSeconds(barIndexForDay * barSizeInTicks)
                       let barEndDateTime = tick.Timestamp.Date.Add(natural_time).AddSeconds((barIndexForDay + 1) * barSizeInTicks)
                       group tick by new { barBeginDateTime, barEndDateTime, tick.Symbol } into tickGroup
                       orderby tickGroup.Key.barBeginDateTime
                       let orderedPrices = tickGroup.OrderBy(t => t.Timestamp).Select(t => t.Price)
                       select new OHLCBar()
                       {
                           Symbol = tickGroup.Key.Symbol,
                           OPEN = orderedPrices.First(),
                           CLOSE = orderedPrices.Last(),
                           HIGH = orderedPrices.Max(),
                           LOW = orderedPrices.Min(),
                           VOLUME = Convert.ToUInt32(tickGroup.Sum(t => t.Volume)),
                           BeginTime = tickGroup.Key.barBeginDateTime,
                           EndTime = tickGroup.Key.barEndDateTime,
                           Interval = new TimeSpan(0, 0, (int)barSizeInTicks)
                       };
            return bars;
        }
  • 台股交易日期則需要至臺灣證券交易所的OpenAPI查詢。筆者拼湊了個簡單的抓取程式如下。但是必須注意的是臺灣證券交易所把春節年假前的交易日期混在停止交易的日期中,所以就算抓下來之後,還要自行修改內容才能直接當作IsHoliday使用。希望有人能反映給臺灣證券交易所,這種資料是給電腦看的,不是給人看的,格式不要亂塞一通。
        private async Task GetHolidayInfo(string path)
        {
            string req = "https://openapi.twse.com.tw/v1/holidaySchedule/holidaySchedule";
            string csv = await GetHttpData(req);
            File.WriteAllText(path, csv);
        }
        private async Task<string> GetHttpData(string weblink)
        {
            HttpClientHandler handler = new HttpClientHandler();
            handler.UseDefaultCredentials = true;

            HttpClient client = new HttpClient(handler);

            try
            {
                HttpResponseMessage response = await client.GetAsync(weblink);

                response.EnsureSuccessStatusCode();

                string responseBody = await response.Content.ReadAsStringAsync();
                string csv = jsonToCSV(responseBody, ",");

                return csv;
            }
            catch (HttpRequestException e)
            {
                Debug.WriteLine("Exception Caught! Message :{0} ", e.Message);
                return null;
            }
        }
        private static DataTable jsonStringToTable(string jsonContent)
        {
            DataTable dt = JsonConvert.DeserializeObject<DataTable>(jsonContent);
            return dt;
        }
        private static string jsonToCSV(string jsonContent, string delimiter)
        {
            StringWriter csvString = new StringWriter();
            var configuration = new CsvConfiguration(CultureInfo.InvariantCulture)
            {
                Delimiter = delimiter,
                MissingFieldFound = null
            };
            using (var csv = new CsvWriter(csvString, configuration))
            {                
                using (var dt = jsonStringToTable(jsonContent))
                {
                    foreach (DataColumn column in dt.Columns)
                    {
                        csv.WriteField(column.ColumnName);
                    }
                    csv.NextRecord();

                    foreach (DataRow row in dt.Rows)
                    {
                        for (var i = 0; i < dt.Columns.Count; i++)
                        {
                            csv.WriteField(row[i]);
                        }
                        csv.NextRecord();
                    }
                }
            }
            return csvString.ToString();
        }
  • 月結算的交易時間與平常日不同,月結算當日比較早收盤,同時還有轉倉的問題,所以筆者也拼湊判斷月結算的函式如下:
        public static bool IsMonthExpDate(this DateTime d)
        {            
            if (GetMonthExpDate(d).Date == d.Date)
            {
                return true;
            }
            return false;
        }
        private static DateTime GetMonthExpDate(DateTime date) // 取得月份到期日(當月第3個星期三)
        {
            // 本月第一天
            DateTime firstDay = new DateTime(date.Year, date.Month, 1, 14, 0, 0);
            // 找到第一個星期三
            while (firstDay.DayOfWeek != DayOfWeek.Wednesday)
            {
                firstDay = firstDay.AddDays(1);
            }
            firstDay = firstDay.AddDays(14);
            // 長假順延
            while (IsHoliday(firstDay)) 
            {
                firstDay = firstDay.AddDays(1);
            }
            return firstDay;
        }
        public static string GetFutureYM(DateTime date) // 取得該日應交易的年月份
        {
            int year = Convert.ToInt32(date.Year.ToString());
            int month = date.Month;
            DateTime monthExp = GetMonthExpDate(date);
            if (date > monthExp)
            {
                month += 1;
                if (month == 13)
                {
                    month = 1;
                    year = year + 1;
                }
            }
            string FutureYM = year + month.ToString("00");
            return FutureYM;
        }
  • 資料暫存區,因為Tick資料筆數太多,直接存放在SSD硬碟裡會造成讀寫遲緩以及影響SSD硬碟壽命的問題,所以筆者利用AMD Radeon(TM) RAMDisk在DDR記憶體中開了個Z槽當作頻繁資料存取之用。每天早上5點夜盤結束之後,才備份回一般的硬碟裡。當然這期間要祈禱主機沒當機或是斷電關機。
至於交易策略部分,筆者嘗試過一些天馬行空的交易策略,是還沒找到甚麼聖杯,大部分的交易策略幾乎都是賠錢的。為了本文的完整度,筆者參考一篇透過三種指標綜合所打造的交易策略Trading Stocks using Bollinger Bands, Keltner Channel, and RSI in Python範例改寫為C#版本當作台指期5分K交易進出邏輯。進場條件為布林通道上下緣超過肯特納通道上下緣,而且RSI相對強弱指標需要超過70或是低於35。出場條件則是布林通道上下緣低於肯特納通道上下緣或是RSI相對強弱指標回到70至35中間震盪。進出場濾網則是加上了一個ATR價格波動。簡單說就是把肯特納通道當作震盪區間,一旦突破這個區間就開始順勢買多或是賣空,回到這個區間就平倉。
實作完這個簡易的策略之後,當然就是要開始跑回測。回測數據要去哪裡找呢? 還好可以透過群益API下載過去一年份的1分K歷史資料當作回測數據,但是一年份的資料代表性其實是存疑的,不過筆者也沒甚麼好方法,也懶得去撈其他來源的歷史資料,還要驗證資料正確性與格式轉換,只好死馬當活馬醫,透過模擬現在系統時間的方式去餵1分K資料給這個策略執行,以一口大台的成本得到的結果扣除手續費大概就是一千點上下,平均每個工作天可以賺個五點,換算大台指就是台幣一千塊錢,最高連續虧損會落在一百多點,也就是兩萬多塊台幣。老實說,執行這個策略不如不要執行,去超商老老實實的打工比較實在,但是滿足了技術宅探索量化交易的願望。
跑完回測,至少看到期望值是正的。接下來就是模擬下單,到這邊就會發現滑價的問題,用限價去買賣成交常常不成功,只好透過範圍市價的方式交易,然後開始祈禱不要賠太多。以下是群益API設定範圍市價的方法。
        public int OrderFuture(string strStockNo, string strPrice, int nQty, short nBidAsk, short type, ref string strMessage)
        {            
            FUTUREORDER pOrder = new FUTUREORDER()
            {
                bstrFullAccount = m_UserAccount,
                bstrPrice = "P", // 委託價格,「M」表示市價,「P」表示範圍市價
                bstrStockNo = strStockNo,
                nQty = nQty,
                sTradeType = 1, //0:ROD  1:IOC  2:FOK
                sBuySell = nBidAsk, //0:買進 1:賣出
                sDayTrade = 0, //當沖0:否 1:是,可當沖商品請參考交易所規定。
                sNewClose = type, //新平倉,0:新倉 1:平倉 2:自動{新期貨、選擇權使用}
                sReserved = 0  //{期貨委託SendFutureOrderCLR適用}盤別,0:盤中(T盤及T+1盤);1:T盤預約
            };

            m_nCode = OnFutureOrderSignal(m_strLoginID, false, pOrder, ref strMessage);

            return m_nCode;
        }
在連續跑了幾星期的模擬下單後發現,好像沒有賠太多,運氣好的時候,還真的有賺一些。於是筆者捏緊了懶趴心一橫就開始實單交易,果然不出意外的話就要出意外了,實單交易一直連續賠錢到現在,也不知道回測中的賺錢時間甚麼時候會到來。真正達到第一次量化交易投資就賠錢的境界。
做個總結,因為本文所提及的交易策略較為粗陋,大部分時間都是在等待,也沒有順勢加碼的策略(其實筆者本身也沒有加碼的本錢),所以獲利的空間相當有限,另外台股特性本來就是震盪居多,波段走勢較少,也導致這個策略需要更多的耐心與信心去執行。好處就是電腦自動執行,萬一有個波段行情的時候,電腦程式能夠自行去下單交易,壞處就是獲利期望值太少,甚至扣除手續費與滑價問題的話,還會賠錢,不如其他長期持有的策略賺得還比較多。筆者自己還是趁工作閒暇之餘,找尋其他交易策略,希望有朝一日能夠真正實現財富自由的願望。
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.