Skip to main content

【程式交易實戰 09】萬能的出場心法 - 移動停利實作

· 9 min read

歡迎來到 程式交易實戰 的第九堂課,還記得上堂課實作的停損停利方法嗎?在交易策略中,停損停利扮演著相當重要的角色,因此上堂課實作了基本的停損停利實作演練以及結合 Line Notify 接收通知提醒,這堂課將延續停損停利的主題,帶大家實作更能夠應用在實際交易操作的移動停利停損方法!

讀完本篇文,您將學會...

  • 透過庫存個股進行移動停損停利實單演練

移動停利停損簡介

相信大家多少都聽過停損停利的出場策略,這邊舉個短線交易的情境:當我看好一檔標的並判斷該股票大約有 20 % 的報酬,因此設定買進股票後報酬率超過 20% 就賣出獲利了結;當報酬率低於 -10%,就會認賠出場,示意圖如下:

week9_01.png

但現實通常並非如此,不知道讀者是否曾遇過到達停利點後,股價仍持續走強後續的獲利都沒有參與到,或是還沒到達停利點就開始下跌,最後碰到停損點只能忍痛出場,示意圖如下:

week9_02.png

那該怎麼避免上述例子發生呢?

移動停利法主要就是能夠解決上述問題。移動停利(Trailing Stop),又稱移動鎖利,也有人稱為動態停利,主要是隨著獲利創新高後回檔特定 % 數或價格,來進行停利出場操作,是一種能夠避免大量獲利回吐下仍能守住獲利的出場策略,示意圖如下:

week9_03.png

上圖的橘色虛線即為移動停利線,當最新價格跌破該條線就會進行停利操作。移動停利的優勢在於能夠根據市場變化來自動調整,會隨著獲利創新高而逐步調整停利點的一種停利策略。因此,本篇文章將帶大家實作相當實用的移動停利方法,並搭配庫存進行實單演練!

策略實作

我們廢話不多說,直接進入策略實作部分吧!首先移動停利會需要取得一段時間的價格高點,因此第一部分需取得策略開始後到最近一個交易日的最高價格,示意圖如下:

week9_04.png

程式碼架構預計從歷史 Candles API 中取得,接著若盤中即時價格高於一段時間的歷史最高價時,最高價將會被即時價格的最高價所取代。

step1. 載入相關套件

# 載入行情 API 套件
from fugle_marketdata import WebSocketClient, RestClient

from configparser import ConfigParser
from fugle_trade.sdk import SDK

from fugle_trade.order import OrderObject
from fugle_trade.constant import (APCode, Trade, PriceFlag, BSFlag, Action)

# 載入相關套件
import math
import datetime
import json
import pandas as pd

step2. 設定參數

symbolId = '2330' # 可以輸入您庫存中的某檔個股
SP = 0.1 # 設定最高價向下回檔 10% 進行停利

start_date = '2023-01-01' # 預計執行的策略日期

step3. 程式碼實作

class TrailingStopStrategy:

# 設定方便調整的參數有 股票代碼, 移動停利 %
def __init__(self, symbol_id, stopProfit_ratio):

# 初始化交易 API 並取得 sdk object
self.sdk = self._init_fugle_trade()

self. API_KEY = "INPUT_YOUR_API_KEY" # INPUT_YOUR_API_KEY

# 設定交易標的
self.symbol = symbol_id

# 設定停利比例
self.stopProfit_ratio = stopProfit_ratio

# 最新價格
self.now_price = None

# 交易 API 設定檔的部分
def _init_fugle_trade(self):
# 讀取設定檔
config = ConfigParser()
config.read('./config.ini') # 使用正式環境
# 登入
sdk = SDK(config)
sdk.login()

return sdk

# 賣出設定
def sell(self, qty, PriceFlag_type, APCode_type):

order = OrderObject(
buy_sell=Action.Sell,
price_flag=PriceFlag[PriceFlag_type],
price = '',
stock_no=self.symbol,
quantity=qty,
ap_code=APCode[APCode_type],
trade=Trade.Cash # 現股賣出 的交易類別
)

self.sdk.place_order(order)

# 平倉
def sell_total_shares(self, qty):

if qty >= 1000:
# 計算預計賣出張數 -> 小數點無條件捨去
vol = math.floor(qty/1000)
# 市價、整股賣
# self.sell(vol, 'Market', 'Common')
# 測試用、漲停賣(避免真的賣掉)
self.sell(vol, 'LimitUp', 'Common')

# 若還有零股需再下零股單
# 賣出剩下的零股
odd_vol = qty - vol*1000
if odd_vol > 0:
# 跌停價、盤中零股賣
# self.sell(odd_vol, 'LimitDown', 'IntradayOdd')
# 測試用、漲停賣(避免真的賣掉)
self.sell(odd_vol, 'LimitUp', 'IntradayOdd')

# 庫存不足一張時
elif qty < 1000:
# 跌停價、盤中零股賣
# self.sell(qty, 'LimitDown', 'IntradayOdd')
# 測試用、漲停賣(避免真的賣掉)
self.sell(qty, 'LimitUp', 'IntradayOdd')

# 取得庫存股數
def get_inventoryInfo(self):

inventories_list = self.sdk.get_inventories()

# 取得庫存股票股數
spec_symbol = list(filter(lambda x:x['stk_no']==self.symbol, inventories_list))
qty = int(spec_symbol[0]["cost_qty"])

return qty

# 取得一段時間的最高價
def get_high_price(self, start_date):

end_date = datetime.datetime.now().date().strftime("%Y-%m-%d") # 最新一個交易日

# 取得歷史 Candles API: https://developer.fugle.tw/docs/data/http-api/historical/candles

rest_client = RestClient(api_key = self.API_KEY) # 輸入您的 API key
stock = rest_client.stock # Stock REST API client

historical_data = stock.historical.candles(**{"symbol": self.symbol,
"from": start_date,
"to": end_date,
"fields": "high"})['data']

high_price = pd.DataFrame(historical_data)['high'].max()

return high_price


# 透過下面的 webSocket function 取得最新價格
def get_latest_price(self, message):

json_data = json.loads(message)

print(json_data)

if json_data['event']=="data" and json_data.get('data',{}).get('isTrial') == None: # 避免用到試撮資料
# 更新目前價格
self.now_price = json_data['data']['price']

print('最新價格:', self.now_price)

self.run_strategy(self.qty)


elif json_data['event']=="snapshot": # 盤後測試
# 更新目前價格
self.now_price = json_data['data']['price']

print(self.now_price)

self.run_strategy(self.qty)


def handle_disconnect(self, code, message):
print(f'disconnect: {code}, {message}')


def main(self, start_date):

self.qty = self.get_inventoryInfo()

# start_date = '2023-01-01'# 策略起始日
self.high_price = self.get_high_price(start_date)

client = WebSocketClient(api_key=self.API_KEY)
self.stock = client.stock
self.stock.on('message', self.get_latest_price)
self.stock.on("disconnect", self.handle_disconnect)

self.stock.connect()

self.stock.subscribe({
'channel': 'trades',
'symbol': self.symbol
})

# 移動停損停利判斷

def run_strategy(self, qty):

try:
print('get_run_strategy start!')

if self.now_price is not None:

# 盤中需判斷目前價位是否為更新價位
if self.now_price >= self.high_price:

self.high_price = self.now_price

print("最新價格:", self.now_price)
print("最高價:",self.high_price)

# 達到 停利 就進行平倉操作:
if self.now_price <= self.high_price*(1 - self.stopProfit_ratio):

# 平倉
# self.sell_total_shares(qty)

msg = '\n'+ f'達到移動停利條件:{self.stopProfit_ratio*100} %'+'\n'+'賣出' + str(self.symbol) +'\n'+ str(self.now_price) +'元' + '\n' + str(qty) + "股"

print(msg)
#line notify 通知
lineTool.lineNotify(self.LINE_NOTIFY_TOKEN, msg)
self.stock.disconnect() # 達條件,即可斷線

else:
msg = '\n'+ f'尚未達到移動停利條件!'+'\n'+'現價:' + str(self.now_price) +' 元' + '\n' +'股數:'+ str(qty) + " 股"
print(msg)

except Exception as e:
print('get_run_strategy error: {}', e)



if __name__ == '__main__':
strategy = TrailingStopStrategy(symbolId, SP)
strategy.main(start_date)

結論

本篇文章帶大家了解如何實作移動停利出場策略,並搭配庫存個股進行停損停利的實作演練,希望能夠幫助讀者更了解富果 API 的出場應用!另外,讀者也可以嘗試做更多的變化,例如:最高點向下回檔 n % 就賣出一半的個股,來進行分批出場操作,快去試試吧!

下篇文章將會是程式交易實戰系列的最終篇,會談談有關多策略的投資組合,也許可以透過多策略的互補性解決單一策略常遇到的劣勢,請大家持續關注!