Backtesting Part3: Adjusting entry exit prices

หลังจากเพิ่มเติม stoploss ไปแล้ว เราจะมาเพิ่มรายละเอียดกับการ Backtest กับอีกซักหน่อยหนึง จากบทความที่แล้ว ยังมีความไม่สมจริงอยู่บางอย่างคือ การคิดกำไรเมื่อตอนเราเปิดสัญญา Long หรือ Short

(df["Close"][i]/df["Close"][i-1])-1   

การคิดคำนวณแบบนี้มันคงไม่มีปัญหาอะไรถ้าเราจริงๆ ถ้าเรามีสัญญา Long อยู่ หุ้นขึ้นจาก 10 บาท ไป 12 บาทเราก็ควรจะได้กำไร (12/10)-1 = 0.2 หรือคิดเป็นกำไร 20% อยู่แล้วถ้าเรามีหุ้นตัวนั้นอยู่ใน Portfolio ของเรา แต่มันยังมีเคสความไม่สมจริงอยู่ คือในกรณีที่เราเข้าซื้อวันแรก สมมุติว่า เรามีสัญญาณซื้อวันที่ 5มกราคม เราก็จะต้องไปซื้อหุ้นเข้าพอร์ตนะวันที่ 6 หรือวันถัดมานั่นเอง

ถ้าราคาหุ้น

  • วันที่ 5 ปิดที่ 10 บาท
  • ราคาเปิดของวันที่ 6 เปิดที่ 11 บาท และไปปิดที่ 12 บาท

ในกรณีนี้ การคำนวณโดยใช้ (ราคาปิดเมื่อวาน/ราคาปิดวันนี้)-1 ย่อมไม่ถูกต้อง เพราะราคามันเปิดที่ 11 บาท!!! มันไม่มีทางเลยที่เราจะได้หุ้นนั้นมาในราคา 10 ถ้าเราซื้อตอนราคาเปิดของวันพรุ่งนี้เว้นแต่เราจะเจอแรงเทขายตอนเช้าแล้วราคาตกลงมาที่ 10 จริงๆแล้วเราบังเอิญได้ราคานั้นพอดี แต่จริงๆแล้ว โอกาสที่เราจะได้หุ้นราคาเปิดวันนี้มา ในราคาเดียวกับราคาปิดเมื่อวาน มันไม่ได้มีมากนัก ปัญหานี้มันก็จะเป็นแบบนี้เช่นเดียวกับกรณีที่เรามีเปิดสัญญา Short ครั้งแรก อีกทั้ง

อีกกรณีคือการปิดสัญญาครับ ครึ่งบางครั้งเมื่อเราปิดสัญญาซื่อขาย ถ้าเราเจอ stoploss วันที่ 6 เราต้องไปปิดสัญญาใน วันที่ 7 เราเองอาจจะไม่ได้อยากรอให้จนถึงจบวันแล้วค่อยคิดกำไรขาดทุนก็ได้ เราอาจจะแค่รีบๆออกมาเลยก็ได้

ฉะนั้นใน part ที่ 3 ของชุดบทความนี้ เราจะมาเพิ่มเติมวันเข้าเปิดสัญญา และ ซื้อและขาย ครั้งแรก + ปิดสัญญาในกรณีเจอ stoploss หรือ Counter Trend กันครับ

วันที่เกิด Counter Trend หรือสัญญาณตรงข้ามกับสัญญาณเดิม เราจะต้องคิดกำไรสองแบบ คือ คิดกำไรของการปิดสัญญานั้นๆในวันนั้นก่อน แล้วเราค่อยมาคิดสัญญาการเปิดสัญญาใหม่ครั้งแรก

ก่อนอื่นเราจะไปที่ฟังก์ชั่น signal_creator ของเรากันก่อนเลย

def signal_creator(params):
    transaction = ""
    df = params["df"].copy()
    i = params["i"]   
    signal = params["signal"]
    prev_signal = params["signal"]
    
    if signal != "Buy":
        if (df["sma_fast"][i]>=df["sma_slow"][i]) & (df["sma_fast"][i-1] < df["sma_slow"][i-1]) :
            if prev_signal == "Sell":
                signal = "Sell_to_Buy"
            else:
                signal = "Buy" 
            print(df.index[i], signal, "signal created!!!")
            transaction = "first"
            
    if signal != "Sell":    
        if (df["sma_fast"][i]<=df["sma_slow"][i]) & (df["sma_fast"][i-1]>df["sma_slow"][i-1]):
            if prev_signal == "Buy":
                signal = "Buy_to_Sell"
            else:
                signal = "Sell"
            print(df.index[i], signal, "signal created!!!")
            transaction = "first"
            
    if signal == "Buy" and transaction =="":      
        if df["Close"][i] < df["Close"][i-1] - df["ATR"][i-1]*2:
            signal = "Stop_Buy"     
            print(df.index[i], signal, "signal created!!!")
            
    if signal == "Sell"and transaction =="":        
        if df["Close"][i] > df["Close"][i-1] + stock["ATR"][i-1]*2:
            signal = "Stop_Sell"    
            print(df.index[i], signal, "signal created!!!")
            
    return signal, transaction        

สิ่งที่เพิ่มเข้ามาไม่ได้มีอะไรมา อันแรกคือตัวแปร transaction = “” ให้เป็นค่าตั้งต้น จากนนั้นเมื่อมีสัญญาณใดๆเป็นครั้งแรกเราจะให้ค่าเป็น transaction = “first” เพื่อบอกโปรแกรมว่าเรากำลังมีสัญญาณครั้งแรก แล้วเราไปเพิ่มตรงการ return ของ Function ให้เป็น return signal, transaction

เพิ่มตัวแปร prev_signal = params[“signal”] เพื่อเช็คว่า สัญญาก่อนหน้านั้นเป็นสัญญาอย่างไร เพราะในกรณีที่เรามีสัญญา ซื้อหรือขาย อยู่ใน portfolio ของเราอยู่แล้ว ถ้ามี signal ใหม่ขึ้นมา แล้วมันไม่ตรงกับ signal เดิม เราก็ให้สัญญาณใหม่ขึ้นมา เช่นในกรณีที่ signal เป็น Buy อยู่แล้วเราไปเจอสัญญาณ Counter Trend คือไปเจอสัญญาณให้เราทำการเปิดหน้า Short ขึ้นมา เราจะให้สัญญาณเป็น

    if signal != "Sell":    
        if (df["sma_fast"][i]<=df["sma_slow"][i]) & (df["sma_fast"][i-1]>df["sma_slow"][i-1]):
            if prev_signal == "Buy":
                signal = "Buy_to_Sell

ตามโค้ดส่วนนี้ ถ้ามีสัญญาณตรงข้ามกับสัญญาณเดิม ถ้าเรามีสัญญาณ Buy อยู่ แล้วมีสัญญษณ Sell มาเราจะเปลี่ยน Signal เป็น “Buy_to_Sell” เพื่อการคิดกำไรอกีรูปแบบหนึงต่อไป เราจะทำเช่นเดียวกันกับอีกฝั่ง

    if signal != "Buy":
        if (df["sma_fast"][i]>=df["sma_slow"][i]) & (df["sma_fast"][i-1] < df["sma_slow"][i-1]) :
            if prev_signal == "Sell":
                signal = "Sell_to_Buy"

แล้วก็ตรง stoploss ผมได้เพิ่มเป็นสัญญาณ if signal == “Buy” and transaction ==””: ไว้ด้วยเพราะเหตุผลที่ว่า วันที่เรามีสัญญาณซื้อเป็นครั้งแรกผมจะไม่มาเช็ค Stoploss นั่นเอง

จากนั้นเราไปขั้นตอนที่ main loop ของโปรแกรมของเรา

def strategy(stock):
    
    df = stock.copy()
    signal = ""
    ret = []
    
    for i in range(0,len(df)):
        params ={"df": df, "i": i, "signal": signal}
        
        if signal == "":
            ret.append(cal_ret(params, opt="No_Trade"))
            if i > 0:
                signal, transaction = signal_creator(params)
            
        elif signal == "Buy": 
            if transaction == "first":
                ret.append(cal_ret(params, opt="First_Buy"))
            else:
                ret.append(cal_ret(params, opt="Buy"))            
            signal, transaction = signal_creator(params)

        elif signal == "Sell": 
            if transaction == "first":
                ret.append(cal_ret(params, opt="First_Sell"))  
            else:
                ret.append(cal_ret(params, opt="Sell"))  
            signal, transaction = signal_creator(params)    

        elif signal == "Stop_Buy":  
            ret.append(cal_ret(params, opt="Stop_Buy"))
            signal = "" 
    
        elif signal == "Stop_Sell":
            ret.append(cal_ret(params, opt="Stop_Sell"))
            signal = ""  
        
        elif signal == "Buy_to_Sell" :   
            ret.append(cal_ret(params, opt="Stop_Buy")+cal_ret(params, opt="First_Sell") )
            signal = "Sell"
            params.update({"signal" : signal})
            signal, transaction = signal_creator(params)
        
        elif signal == "Sell_to_Buy" :   
            ret.append(cal_ret(params,opt="Stop_Sell")+cal_ret(params,opt="First_Buy") )
            signal = "Buy"
            params.update({"signal" : signal})
            signal, transaction = signal_creator(params)    
        
    df["strategy"] = np.array(ret)             
    return df

ที่ main loop ของเราผมเปลี่ยนมันเป็น Function การเรียนหาสัญญาณใน signal_creator เราก็จะเพิ่มตัวแปรมารับค่า transaction เพิ่มมา 1 ตัว เป็นรูปแบบนี้

signal, transaction = signal_creator(params)

และผมได้มีการเรียกหา Function cal_ret สำหรับการซื้อขายแต่ละครั้งผมจะเช็ดด้วยว่าการได้สัญญาณครั้งนั้นเป็นการได้สัญญาณซื้อขายครั้งแรกหรือไม่ ถ้าใช่ผมจะเรียก

elif signal == "Buy": 
    if transaction == "first":
        ret.append(cal_ret(params, opt="First_Buy"))
    else:
        ret.append(cal_ret(params, opt="Buy"))            
    signal, transaction = signal_creator(params)

เราจะเช็คว่าถ้าสัญญาณที่ได้มาเป็น trasaction ครั้งแรกหรือไม่ ถ้าใช่ เราก็จะเรียก cal_ret(params, opt=”First_Buy”) เพราะเราจะซื้อมันเป็นครั้งแรกด้วยราคาเปิดของมัน(เพราะคิดกำไรจากราคาปิดเมื่อวานมันไม่ถูกต้อง เมื่อวานเราไม่ได้มีหุ้นนี้ในportfolio) ถ้าไม่ใช่ ก็แปลว่าเรามีหุ้นอยู่ใน portfolio แล้วก็ไปเรียก cal_ret(params, opt=”Buy”) ธรรมดา

จากนั้นไปที่โค้ดที่เพิ่มมาใหม่ในส่วนของ elif signal == “Buy_to_Sell” : และ elif signal == “Sell_to_Buy” : คือสองกรณี

  • การปิดสัญญา Buy และเปิดสัญญา Sell
  • การปิดสัญญา Sell และเปิดสัญญา Buy
 elif signal == "Buy_to_Sell" :   
            ret.append(cal_ret(params, opt="Stop_Buy")+cal_ret(params, opt="First_Sell") )
            signal = "Sell"
            params.update({"signal" : signal})
            signal, transaction = signal_creator(params)
        
        elif signal == "Sell_to_Buy" :   
            ret.append(cal_ret(params,opt="Stop_Sell")+cal_ret(params,opt="First_Buy") )
            signal = "Buy"
            params.update({"signal" : signal})
            signal, transaction = signal_creator(params)    

ในขั้นตอนนี้เราจะทำสองอย่าง คือปิดสัญญาเดิม และ เปิดสัญญาใหม่ ในกรณีที่เราจะปิดสัญญา Buy ไปเปิดสัญญา Sell ผมจะต้องคิดกำไรสองแบบด้วยกันคือ cal_ret(params, opt=”Stop_Buy”) + cal_ret(params, opt=”First_Sell”) กล่าวเป็นภาษาคนเราก็คือ เมื่อวันที่ผมมีสัญญาณ Sell แล้วผมมีหุ้นอยู่ใน Portfolio ของผม เช้าวันต่อมาผมจะรีบปิดสัญญาญ คือผมจะขายหุ้นออกไปที่ราคาเปิดเลย ด้วยการคิด cal_ret(params, opt=”Stop_Buy”) พอขายออกไปแล้วในวันนั้นมันจะไม่จบแค่นั้น เพราะผมต้องเปิดสัญญา Sell ด้วยเช่นกัน ผมก็จะคิดกำไรทางฝั่ง Sell จากราคาเปิดของมันเช่นกัน จึงเรียกคำสั่ง cal_ret(params, opt=”First_Sell”) มาซึ่งเราจะทำเช่นเดียวกันกับฝั่ง Sell_to_Buy

โอเคครับ มาถึงช่วงนี้แล้วผมว่าเราน่าจะเริ่มมาคิดกำไร calret ของเรากันแล้ว

def cal_ret(params, opt):
    
    df = params["df"]
    i = params["i"]
    
    if opt == "No_Trade":
        ret = 0
    elif opt == "Buy":    
        ret = (df["Close"][i]/df["Close"][i-1])-1    
    elif opt == "Sell":  
        ret = 1-(df["Close"][i]/df["Close"][i-1])
    elif opt == "Stop_Buy":     
        ret = (df["Open"][i]/df["Close"][i-1])-1
    elif opt == "Stop_Sell":     
        ret = 1-(df["Open"][i]/df["Close"][i-1])
    elif opt =="First_Buy":
        ret = (df["Close"][i]/ df["Open"][i])-1
    elif opt == "First_Sell":
        ret = 1-(df["Close"][i] /df["Open"][i])   

    print(df.index[i], opt,"return: ",round(ret*100,2),"%") 
    
    return ret

ตรงนี้มันก็จะ Make Sense นะครับว่าทำไมบทความก่อน cal_ret opt == “Sell” และ opt == “Stop_sell” กับ opt == “Buy” และ opt = “Stop_Buy” ทำไมอย่างละสองตัวทั้งๆที่ด้านในเหมือนกัน จริงๆคือผมทำไว้ก่อนละจะเพิ่มรายละเอียดตอนนี้ครับ

    elif opt == "Stop_Buy":     
        ret = (df["Open"][i]/df["Close"][i-1])-1
    elif opt == "Stop_Sell":     
        ret = 1-(df["Open"][i]/df["Close"][i-1]

ส่วนของ Stop_Buy และ Stop_Sell ผมจะตีความว่า ไม่ว่าเราจะได้สัญญาณ Counter trend หรือ Stoploss มา ผมก็จะรีบขายมันออกไปที่ราคาเปิดมันเลย ผมไม่อยากถือไว้จนหมดวันผมจริงคิดกำไรโดยการใช้ (df[“Open”][i]/df[“Close”][i-1])-1 ในฝั่ง Stop_Buy และ 1-(df[“Open”][i]/df[“Close”][i-1] ในฝั่ง Sell

แล้วเราก็จะไม่ได้รีบแค่ฝั่ง Stop แต่เราจะรีบซื้อในฝั่งการซื้อครั้งแรกด้วย

    elif opt =="First_Buy":
        ret = (df["Close"][i]/ df["Open"][i])-1
    elif opt == "First_Sell":
        ret = 1-(df["Close"][i] /df["Open"][i])  

First_Buy เราใช้ (df[“Close”][i]/ df[“Open”][i])-1 คือ (ราคาปิดวันนี้/ราคาเปิดวันนี้)-1 เพราะวันนี้ว่ากำไรในการเข้าซื้อครั้งแรกของเราเราจะได้กำไรก็ต่อเมื่อ ราคาปิดวันนั้น สูงกว่าราคาเปิดที่เราซื้อมาตอนเช้า

First_Sell ก็เช่นกันเราใช้ 1-(df[“Close”][i] /df[“Open”][i]) คือ 1-(ราคาปิดวันนี้/ราคาเปิดวันนี้) ถ้าเรา Short ในวันนี้เราจะได้กำไรก็ต่อเมื่อ ราปิดวันนี้มันต่ำกว่าราคาเปิดเมื่อเช้า

ใส่รายละเอียดแล้วก็รันมันครับ

stock = strategy(stock)

วัดผลเทียบกับ Buy and Hold

print("pnl",((1+stock["strategy"]).cumprod()[-1]-1)*100)
plt.plot((1+stock["strategy"]).cumprod(), label="Strategy", color = 'green')
plt.plot((1+bnh["rets"].fillna(0)).cumprod(), label="BnH", color = 'black')
plt.title(symbol+" strategy return")
plt.legend()
plt.show()
ผลงานหลังจาก adjust ราคาเปิดสัญญาและปิดสัญญา

และก็นี่คือผลงานหลังจากเราปรับนู้นนิดนี่หน่อย ปิดสัญญาให้เร็วขึ้น + เปิดสัญญาคิดกำไรให้สมจริง ดูเหมือนผลลัพธ์จะดีขึ้นมานิดหน่อย อันนี้ไม่ใช่ประเด็นครับ แล้วแต่ดาต้า ถ้าเป็นดาต้าอื่นผลอาจจะแย่ลงก็ได้ แต่ประเด็นคือเรากำลังทำมันให้มีรายละเอียดที่สมจริงเพิ่มขึ้นนั่นเอง part หน้าเราจะเพิ่มรายละเอียดให้กับการเทรดไปอีกด้วยการจัดเก็บ trading log เพื่อเก็สถิติเพิ่มเติมนะครับ

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s