Python {Article118}

ようこそ「Python」へ...

ITエンジニアが仮想通貨の自作自動売買システム(bot)で月収7万円のパッシブインカムを得るには [10] BOTのBuySellクラスを作成する

ここではITエンジニアがPythonで仮想通貨の自動売買システム(BOT)を作成してパッシブインカム(Passive Income)を得る方法を解説します。 「仮想通貨のボット(BOT)でパッシブインカムを得る」シリースは以下の11回に分けて解説します。 第10回目ではこのシリーズで作成するBOTのBuySellクラスを作成する方法を解説します。 なお、このシリーズで作成するBOTは日本の取引所「GMOコイン」を利用することを前提にしています。 bitFlyer、Binanceなどの取引所を利用するBOTについては後日、別記事にて解説します。

予め断っておきますが、BOTを作成したからといってすぐには稼げるようにはなりません。 3ヶ月から6ヶ月かけて仮想取引とリアル取引をしてBOTのアルゴリズムとパラメータを調整する必要があります。 そうすれば半年後には投資額にもよりますが月5万円から10万円くらいは稼げるようになっているはずです。

半年経っても月10万円稼げないときは次の「座右の銘」を思い出してください。 きっと稼げるようになるはずです。

「上がり始めたら買え(下がっている間は買うな)。 下がり始めたなら売れ(上がっている間は売るな)。 また、そもそも当てられるわけのない天井や底で売買しようとするな。 一番安いところで買ったり、 一番高いところで売れるものだと思うな。」 このシリーズではMicrosoftのVisual Studio Code(VS Code)を使用しますが、Jupyter NotebookなどのツールでもOKです。 説明文の左側に図の画像が表示されていますが縮小されています。 画像を拡大するにはマウスを画像上に移動してクリックします。 画像が拡大表示されます。拡大された画像を閉じるには右上の[X]をクリックします。 画像の任意の場所をクリックして閉じることもできます。

【免責事項】
当記事や当サイトの情報を参考に仮想通貨のBOT(自動トレードプログラム)を動かす場合は、すべて自己責任でお願いいたします。 当サイトの情報を参考にして発生したいかなる損害も責任を負いません。

BOTのBuySellクラスを作成する前に、予め以下の「オブジェクト指向プログラミング」のシリーズを読んでクラスの基本を理解してください。

BOTのBuySellクラスを作成する

  1. BuySellクラスを作成して__init__(), __str__()メソッドを追加する

    ここではBuySellクラスを作成して__init__(), __str__()メソッドを追加します。 BuySellクラスには、BOTが自動トレードするための各種メソッドがまとめられています。 レバレッジ取引には「買い▶売り」と「空売り▶買戻し」の2種類ありますが、 BuySellクラスでは前者の「買い▶売り」の取引に関連するメソッドがまとめられています。

    __init__()メソッドはBuySellクラスのインスタンスを生成したときに自動的に呼ばれます。 __str__()メソッドはprint()等でBuySellクラスを表示したときに呼ばれます。 __init__()、__str__()の詳しいことは、 「記事(Article085)」、 「記事(Article087)」で詳しく解説しています。

    buy_sell.py:
    # buy_sell.py
    
    """
    BuySell class
    """
    
    # import the libraries
    from time import sleep  
    import datetime as dt
    from datetime import timedelta
    
    import math
    import statistics
    from decimal import Decimal
    
    import numpy as np
    import pandas as pd
    
    from lib.base import Bcolors, Gvar, Coin, Gmo_dataframe 
    from lib.debug import Debug
    from lib.api import Api
    from lib.csv import Csv
    from lib.trade import Trade    
    
    import warnings
    warnings.simplefilter('ignore')
    
    ##############
    class BuySell:
        """  
        __init__(), __str__(),     
        reset_restart_process(), 
        buy_sell(), buy_sell_algorithm_0(), buy_sell_algorithm_1, buy_sell_algorithm_2(), buy_sell_algorithm_3(),...
        create_buy_order(), create_sell_order()
        update_order(), update_closed_order(), update_partially_closed_order(), update_order_profit() 
        stop_loss(), stop_loss_0(), stop_loss_1(),...
        cancel_pending_order()
        close_pending_order()
        """
       
        ######################################################
        def __init__(self, gvar: object, coin: object) ->None: 
            self.gvar = gvar                           
            self.coin = coin                                  
            self.debug = Debug(gvar)                         
            self.api = Api(gvar, coin)                  
            self.csv = Csv(gvar, coin)                          
            self.trade = Trade(gvar, coin)                               
             
            self.folder_gmo =  f'{self.gvar.domain}/{self.gvar.exchange}/'  # desktop-pc/bot/            
            self.folder_trading_type =  f'{self.gvar.domain}/{self.gvar.exchange}/{self.coin.trade_type}/'  # desktop-pc/bot/buy_sell/             
    
            self.debug.print_log(f"{Bcolors.OKGREEN}BuySell({self.gvar.althorithm_id},{self.gvar.buysell_condition}){Bcolors.ENDC} class initiated: {self.coin.symbol}, qty=[{self.coin.qty:,.{self.coin.qty_decimal}f}], rate=[{self.coin.profit_rate:.4f}, {self.coin.stop_loss_rate:.4f}], debug1={self.coin.debug_mode_debug1}, debug2={self.coin.debug_mode_debug2} ")  
            return     
        
        #########################
        def __str__(self) -> str:
            return f"BuySell({self.folder_gmo=}, {self.folder_trading_type=}, {self.gvar.real_mode=}, {self.gvar.reset_mode=}, {self.gvar.althorithm_id=}, {self.coin.symbol=})"

    click image to zoom!
    図1-1
    図1-1はBOTとBuySellクラスの各種メソッドの関連図です。 BOTのmain()から関数「auto_trade()」を呼びます。 auto_trade()では、GMOコインから取引履歴をロードします。 そしてBuySellクラスのbuy_sell()メソッドを呼び出して取引データを渡します。

    buy_sell()メソッドではアルゴリズムを使用して取引のタイミングを決めてcreate_buy_order(), create_sell_order()メソッドを呼びます。 取引の処理が完了するとauto_trade()に戻ります。

    auto_trade()は、次にBuySellクラスのupdate_order()メソッドを呼びます。 update_order()では、update_closed_order()メソッドを呼んで取引が約定(執行)された注文を更新します。 さらにコール元の要求があれば、 stop_loss(), cancel_pending_order(), close_pending_order()等の処理を行います。 最後にupdate_order_profit()メソッドを呼び出して取引の損益を計算してauto_trade()に戻ります。


    click image to zoom!
    図1-2
    図1-2はデモプログラムの実行結果です。 ここではBuySellクラスの各種プロパティを表示しています。 print()でBuySellのオブジェクトを指定するとBuySellクラスの__str__()メソッドが呼ばれます。 __str__()はBuySell, Gvar, Coinクラスから各種プロパティを取得して返します。


  2. BuySellクラスにreset_restart_process()メソッドを追加する

    ここではBuySellクラスにreset_restart_process()メソッドを追加します。 このメソッドはBOTによる自動トレードで必要となる各種CSVファイルを作成して初期化します。

    行47-63はBOTが初期モード(gvar.reset_mode=True)のときに実行されます。 ここでは買い注文と売り注文のCSVファイルを作成して初期化します。 CSVファイルは仮想通貨ごとに作成されます。

    行69-85はBOTに新規に取引する仮想通貨を追加したときに実行されます。 ここでも買い注文と売り注文のCSVファイルを作成して初期化します。

    buy_sell.py:
        ######################################## 
        def reset_restart_process(self) -> None:       
            self.gvar.callers_method_name = 'reset_restart_process():'
            symbol = self.coin.symbol
    
            # get master df from the coin class
            master_df = self.coin.master_df             
            if master_df.shape[0] == 0: 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - master_df is empty {master_df.shape[0]=} return ")
                self.debug.sound_alert(3)    
                return                         
    
            order_qty = self.coin.qty          
            mas_df = master_df                                    
    
            # ---------------------------------------------------------
            #  #   Column     Dtype
            # ---------------------------------------------------------
            #  0   price      float64
            #  1   side       object
            #  2   size       float64
            #  3   timestamp  datetime64[ns, UTC]    
            # --------------------------------------------------------                  
    
            ### get latest buy order info from the GMO dataframe (timestamp is sorted by ascending order)
    
            buy_time = mas_df.iloc[0]['timestamp']      # get oldest time
            buy_price = mas_df.iloc[0]['price']         # get oldest price
            buy_order_id = 'FakeB:' + dt.datetime.now().strftime('%m%d-%H%M%S-%f')      # FakeB:MMDD-HHMMSS-999999(milliseconds)
            buy_position_id = 'PosiB:' + dt.datetime.now().strftime('%m%d-%H%M%S-%f')   # PosiB:MMDD-HHMMSS-999999(milliseconds)            
    
            # round time
            freq = '1T'                           
            buy_time = buy_time.round(freq=freq)                                        # round x minutes (where x is 1,4,10,15,30)   
            buy_time = buy_time.round(freq='S')                                         # round seconds
            buy_time = buy_time - timedelta(hours=1)                                    # substract 1 hour
    
            ### get sell order info from the GMO dataframe
               
            sell_time = buy_time # + timedelta(seconds=5)
            sell_price = self.trade.get_sell_price(buy_price)                                  
            sell_order_id = 'FakeS:' + dt.datetime.now().strftime('%m%d-%H%M%S-%f')      # FakeS:MMDD-HHMMSS-999999(milliseconds)
            sell_position_id = 'PosiS:' + dt.datetime.now().strftime('%m%d-%H%M%S-%f')   # PosiS:MMDD-HHMMSS-999999(milliseconds)     
    
            # reset mode ?
            if self.gvar.reset_mode:      
                self.debug.print_log(f".... {self.gvar.callers_method_name} processing({symbol} reset): delete_allfiles() ▶ add a fake data into the buy/sell order csv files...")
    
                ### 1) add a fake data into the buy order, buy order sub files
    
                # write_buy_order(buy_time: utcnow, sell_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:
                self.csv.write_buy_order(buy_time, sell_time, order_qty, buy_price, order_qty, buy_price, 0.0)                                              
    
                # write_buy_order_sub(buy_time: utcnow, sell_time: utcnow, buy_order_id: str, sell_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None: 
                self.csv.write_buy_order_sub(buy_time, sell_time, buy_order_id, sell_order_id, buy_position_id, order_qty, buy_price, order_qty, buy_price, 0.0)   
    
                ### 2) add a fake data into the sell order, sell sub_order log file
    
                # write_sell_order(sell_time: utcnow, buy_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:     
                self.csv.write_sell_order(sell_time, buy_time, order_qty, sell_price, order_qty, sell_price, 0.0)                     
                
                # write_sell_order_sub(sell_time: utcnow, buy_time: utcnow, sell_order_id: str, buy_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:   
                self.csv.write_sell_order_sub(sell_time, buy_time, sell_order_id, buy_order_id, sell_position_id, order_qty, sell_price, order_qty, sell_price, 0.0)
             
            else: # restart mode
                # self.debug.print_log(f".... processing({symbol} restart): update_order() ▶ add a fake data into the sell/buy order csv files...")
                # master csv file not found ?
                if self.coin.master_csv_not_found:                
                    self.debug.print_log(f".... {self.gvar.callers_method_name} processing({symbol} restart): master_csv_not_found(True) ▶ add a fake data into the buy/sell order csv files...")
                    
                    ### 1) add a fake data into the buy order, buy order sub csv files
    
                    # write_buy_order(buy_time: utcnow, sell_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:
                    self.csv.write_buy_order(buy_time, sell_time, order_qty, buy_price, order_qty, buy_price, 0.0)                                              
    
                    # write_buy_order_sub(buy_time: utcnow, sell_time: utcnow, buy_order_id: str, sell_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None: 
                    self.csv.write_buy_order_sub(buy_time, sell_time, buy_order_id, sell_order_id, buy_position_id, order_qty, buy_price, order_qty, buy_price, 0.0)      
    
                    ### 2) add a fake data into the sell order, sell order sub csv files
    
                    # write_sell_order(sell_time: utcnow, buy_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:     
                    self.csv.write_sell_order(sell_time, buy_time, order_qty, sell_price, order_qty, sell_price, 0.0)                     
                    
                    # write_sell_order_sub(sell_time: utcnow, buy_time: utcnow, sell_order_id: str, buy_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:   
                    self.csv.write_sell_order_sub(sell_time, buy_time, sell_order_id, buy_order_id, sell_position_id, order_qty, sell_price, order_qty, sell_price, 0.0)
                                    
                else: # skip close pending orders
                    self.debug.print_log(f".... {self.gvar.callers_method_name} processing({symbol} restart): skip close pending orders for restart...")                
                # end of if coin.master_csv_not_found:      
            # end of if self.gvar.reset_mode: 
    
            return

    click image to zoom!
    図2
    図2はデモプログラムの実行結果です。 ここではGvarクラスのreset_modeプロパティに「True」を設定して初期化モードで実行しています。 この場合、BOTのサブフォルダ「buy_sell」に買いの注文ファイル「buy_order({symbol}).csv, buy_order_sub({symbol}).csv」 と売りの注文ファイル「sell_order({symbol}).csv, sell_order_sub({symbol}).csv」が作成されて初期化されます。

    CSVファイルは仮想通貨ごとに作成されます。 たとえば、「buy_order(BTC_JPY).csv, buy_order(ETH_JPY).csv, buy_order(BCH_JPY).csv,...」のようなCSVファイルが作成されます。


  3. BuySellクラスにbuy_sell(), buy_sell_algorithm_0()メソッドを追加する

    ここではBuySellクラスにbuy_sell()メソッドを追加します。 このメソッドはBOTの肝になる部分です。 buy_sell()メソッドの引数には「algorithm_id」を指定します。 「algorithm_id=0」はデフォルトで用意されています。 BOTのテストを行うときは「algorithm_id=0」を指定して行います。

    BOTのテストが完了したら独自のアルゴリズムを作成して 新規メソッドとして「buy_sell_algorithm_1()」、「buy_sell_algorithm_2()」のように追加します。 仮想通貨を売買(トレード)するアルゴリズムは、 「記事(Article108)」で紹介しているさまざまなテクニカルインジケーターを組み合わせて作ります。 具体的な新規メソッドの作り方については後述します。

    デフォルトで用意されている「buy_sell_algorithm_0()」メソッドは単純なアルゴリズムを組み込んでいます。 まずは、前回取引(トレード)した注文が約定しているか調べます。 まだ、約定していないときは何もしないで戻ります。 前回の取引が既に約定しているときは、成行きで買いの注文を入れます。 通常、成行きの注文はすぐに約定するので注文が約定するまで待ちます。 買いの注文が約定したら、今度は指値で売りの注文を入れてポジションを決済します。 指値の売りの注文を入れるときの金額は、 単純に買いの金額(レート)に利益率を掛けて計算します。 利益率はCoinクラスの「coin.profit_rate」プロパティに設定されています。

    具体的には、 買いの注文を入れるときは「create_buy_order()」メソッド、 売りの注文を入れるときは「create_sell_order()」メソッドを呼びます。

    buy_sell.py:
        ##############################################   
        def buy_sell(self, algorithm_id: int) -> None:  
    
            if algorithm_id <= 3:
                eval(f'self.buy_sell_algorithm_{str(algorithm_id)}()')    # call 'self.buy_sell_algorithm_?() method
            else:
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}Invalid Algorithm_id {algorithm_id=} {Bcolors.ENDC} -  skip buy_sell ")
                self.debug.sound_alert(3)   
    
            return
    
        #######################################
        def buy_sell_algorithm_0(self) -> None:
            """
     
            """
            self.gvar.callers_method_name = 'buy_sell(0):'  
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal               
            df = self.coin.master2_df
            mas_df = pd.DataFrame(df) # cast dataframe()
    
            ### filter buy master
            filter_mask = mas_df['side'] == 'BUY'
            mas_buy_df = mas_df[filter_mask]
            # mas_buy_df is less than trend_search_count ? => SYTEM ERROR
            if mas_buy_df.shape[0] < self.coin.trend_search_count:  # < 3 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - mas_buy_df({mas_buy_df.shape[0]}) is less than {self.coin.trend_search_count}. skip buy_sell() ")
                self.debug.sound_alert(3)    
                return   
    
            ### filter sell master
            filter_mask = mas_df['side'] == 'SELL'
            mas_sell_df = mas_df[filter_mask]
            # mas_sell_df is less than trend_search_count ? => SYTEM ERROR
            if mas_sell_df.shape[0] < self.coin.trend_search_count:  # < 3 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - mas_sell_df({mas_sell_df.shape[0]}) is less than {self.coin.trend_search_count}. skip buy_sell() ")
                self.debug.sound_alert(3)    
                return 
         
            self.debug.trace_write(f".... {self.gvar.callers_method_name} buy_sell_condition({self.gvar.buysell_condition}), mas_buy_df.shape({mas_buy_df.shape[0]}),  mas_sell_df.shape({mas_sell_df.shape[0]}) ")  
    
            # mas_df:
            # -------------------------------------------------------------
            #  #   Column      Dtype
            # -------------------------------------------------------------
            #  0   time        datetime64[ns, UTC]
            #  1   open        float64
            #  2   close       float64
            #  3   low         float64
            #  4   high        float64
            #  5   price       float64
            #  6   buy_price   float64
            #  7   sell_price  float64
            #  8   side        object
            #  9   size        float64
            #  10  symbol      object
            # ---------------------------------------------------------------
                           
            position = 'buy or sell'                                     
    
            ### load buy order / buy order sub files
            buy_df = self.csv.get_buy_order()                
            buy_sub_df = self.csv.get_buy_order_sub()              
    
            ### load sell order / sell order sub files
            sell_df = self.csv.get_sell_order()            
            sell_sub_df = self.csv.get_sell_order_sub()     
      
            ### update the position depending on buy/sell sub order log files            
    
            ### get the last buy sub order info
            last_buy_time  = buy_sub_df.iloc[-1]['buy_time']  
            last_sell_time = buy_sub_df.iloc[-1]['sell_time']             
            last_buy_real_qty = buy_sub_df.iloc[-1]['real_qty']
            last_buy_real_price = buy_sub_df.iloc[-1]['real_price']        
       
            ### get the last sell order (pair of the buy order)
    
            # find the pair of the sell order
            find_mask = sell_df['sell_time'] == last_sell_time
            dfx = sell_df[find_mask]
            # not found sell order ?
            if dfx.shape[0] == 0:
                # self.debug.print_log(f".... {bcolors.WARNING}{self.gvar.callers_method_name}{bcolors.ENDC} ▶ sell order not found: {last_sell_time=:%Y-%m-%d %H:%M:%S} ")
                last_sell_real_qty = 0
            else: ### found pair of the sell order
                # get sell order real_qty
                ix = dfx.index[0]
                last_sell_real_qty = dfx.loc[ix, 'real_qty']
    
            # closed order ?
            if round(last_buy_real_qty, _q_) == round(last_sell_real_qty, _q_):
                position = 'buy'    # sell order not required => position(BUY) 
                self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}NEW BUY ORDER REQUEST{Bcolors.ENDC} ")     
            else: # not found sell order or incomplete sell order
                return  # skip return         
    
            # buy position ? 
            if position.startswith('buy'):       
                self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}BUY{Bcolors.ENDC} position (long position) ")
                
                ### BUY position only
    
                ### check buy condition and create a buy order
        
                # get the latest time & buy_price from GMO master dataframe
                mas_time  = mas_buy_df.iloc[-1]['time']                  
                mas_price = mas_buy_df.iloc[-1]['price']                    
    
                while True:    
    
                    ###########################################################
                    ### 1: check duplicate buy order
                    ###########################################################
    
                    # mas_time(gmo) is less than or equal to last_buy_time ? => SKIP
                    if mas_time <= last_buy_time: 
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP(1){Bcolors.ENDC} mas_time({mas_time:%Y-%m-%d %H:%M:%S}) is less than or equal to last_buy_time({last_buy_time:%Y-%m-%d %H:%M:%S}) ")      
                        break   # exit while true loop              
                                                        
                    # duplicate buy order ? => SKIP
                    # find_order(df: pd.DataFrame, col: str, val=any) -> bool:
                    if self.trade.find_order(buy_df, col='buy_time', val=mas_time):   
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP(2){Bcolors.ENDC} duplicate buy order: buy_time={mas_time:%Y-%m-%d %H:%M:%S}) ") 
                        break   # exit while true loop 
    
                    ### no duplicate buy order      
    
                    ###########################################################
                    ### 2: execute buy order (MARKET) & sell order (LIMIT)
                    ###########################################################                
    
                    qty = self.coin.qty               
                           
                    ### execute buy order with qty (MARKET) 
                    # create_buy_order(buy_time: utcnow, qty: float, price: float) -> int:  # return status  
                    status = self.create_buy_order(mas_time, qty, mas_price)    # ★ price is used for fake mode: execute buy order with market price
    
                    if status == 0:
                        # reload buy order sub file     
                        buy_sub_df = self.csv.get_buy_order_sub()     
    
                        # get the last buy order sub info
                        last_buy_time  = buy_sub_df.iloc[-1]['buy_time']        
    
                        # get the latest sell price from gmo master
                        ms_price = mas_sell_df.iloc[-1]['price']                                   
    
                        ### execute sell order (LIMIT) 
                        # create_sell_order(sell_time: utcnow, ms_price: float, buy_time: utcnow) -> int:               
                        status = self.create_sell_order(mas_time, mas_price, last_buy_time)  
    
                    break   # exit while true loop            
                # end of while True:                
            
            return      
    
        ####################################### buy_sell : algorithm 1 not implemented  
        def buy_sell_algorithm_1(self) -> None:
            pass
    
        ####################################### buy_sell : algorithm 2 not implemented  
        def buy_sell_algorithm_2(self) -> None:
            pass
    
        ####################################### buy_sell : algorithm 3 not implemented  
        def buy_sell_algorithm_3(self) -> None:
            pass

    click image to zoom!
    図3
    図3はデモプログラムの実行結果です。 画面にはログ情報が表示されています。 BOTからBuySellクラスのbuy_sell(0)メソッドが呼ばれています。 さらにbuy_sell(0)からcreate_buy_order(), create_sell_order()メソッドが呼ばれています。 buy_sell(0)では一定のアルゴリズムに従って買いと売りの注文を出します。

    このbuy_sell()がBOTの肝になる部分です。 buy_sell()には、「記事(Article108)」 で解説しているさまざまなテクニカルインジケーターを組み合わせて新規のメソッドを 「buy_sell_algorithm_1(), buy_sell_algorithm_2(),...」のようなメソッド名で追加します。 なお、デフォルトとして「buy_sell_algorithm_0()」メソッドが用意されています。 このメソッドはBOTをテストするときに使用します。 デモプログラムでは、デフォルトのアルゴリズムを使用しています。


  4. BuySellクラスにcreate_buy_order()メソッドを追加する

    ここではBuySellクラスにcreate_buy_order()メソッドを追加します。 このメソッドでは、新規の買い注文を成行きの指定で入れます。 成行き注文の場合、FAK(Fill and Kill」が適用されて、 全ての数量が執行されないで終了することがあります。 この場合、create_buy_order()では残数の数量に対して再度成行きの注文を入れます。 すべての数量が執行されたらコール元に戻ります。

    buy_sell.py:
        ########################################################################################## 
        def create_buy_order(self, buy_time: dt.datetime.utcnow, qty: float, price: float) -> int:  # return status : price (latest buy market price)        
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal        
            
            self.gvar.callers_method_name = 'create_buy_order():'
            self.debug.trace_write(f".... {self.gvar.callers_method_name} create_buy_order({buy_time=:%Y-%m-%d %H:%M:%S}, {qty=:,.{_q_}f}, {price=:,.{_p_}f}) ")  
            status = 0
    
            # reset buy/sell position count
            self.coin.position_count_0_10 = 0        
            self.coin.position_count_11_30 = 0       
            self.coin.position_count_31_50 = 0      
            self.coin.position_count_51_70 = 0      
            self.coin.position_count_71_90 = 0      
            self.coin.position_count_91_999 = 0     
        
            ################################################
            ### 【1】CHECK DUPLICATE BUY ORDER 
            ################################################
    
            ### check duplicate buy order 
    
            # OPEN (load the buy order file) : buy_order(BTC_JPY).csv 
            buy_df = self.csv.get_buy_order()             
    
            # find a buy order 
            find_mask = buy_df['buy_time'] == buy_time
            dfx = buy_df[find_mask]          
            # duplicate buy order ?     
            if dfx.shape[0] > 0:
                status = 9      # set internal status code 
                self.debug.trace_write(f".... {self.gvar.callers_method_name} duplicate buy order ({buy_time=:%Y-%m-%d %H:%M:%S}) ▶ {status=} error return... ")
                return status   # error return       
    
            # reset sell_position_list
            self.coin.sell_position_list = [] 
    
            #############################################################
            ### 【2】EXECUTE BUY ORDER MARKET (FAK: Fill And Kill)
            #############################################################
    
            ### execute buy order with market price : FAK (Fill And Kill)       
    
            buy_qty = qty       # save original buy qty  
    
            # define buy order header list
            header_real_qty_list = []
            header_real_price_list = []
            header_real_fee_list = []          
    
            status = 0
            loop_count = 0      
    
            while True:
                loop_count += 1
    
                # exe_buy_order_market_wait(qty: float, price: float, latest_close_price=0.0) -> tuple: 
                status, res_df, msg_code, msg_str = self.api.exe_buy_order_market_wait(buy_qty, price)  # ★ FAK (Fill And Kill) : price is used for fake mode  
                res_df = pd.DataFrame(res_df)   # cast           
                self.debug.print_log(f".... {self.gvar.callers_method_name} while({loop_count}): exe_buy_order_market_wait() ▶ {status=}, {res_df.shape=} ")
    
                ####################################################################### DEBUG(1_1) DEBUG(1_1) DEBUG(1_1) : create_buy_order()           
                if not self.coin.real_mode:
                    if self.coin.debug_mode_debug1: 
                        # ★ create two buy order subs for each buy order header ★
                        # update_get_price_response_for_debug1_1(res_df: pd.DataFrame, loop_count: int) -> pd.DataFrame:
                        res_df = self.trade.update_get_price_response_for_debug1_1(res_df, loop_count)   
                        # loop_count == 1: return 2 rows => add 2 buy order subs
                        # loop_count == 2: return 1 row  => add 1 buy order sub                     
                # end of if not self.coin.real_mode:
                ###################################################################### DEBUG(1_1) DEBUG(1_1) DEBUG(1_1) : create_buy_order()
    
                # exe_buy_order_market_wait() error ?
                if status != 0:
                    if status == 1 and msg_code == 'ERR-201': # Trading margin is insufficient ?      
                        self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_buy_order{Bcolors.ENDC}({buy_qty=:,.{_q_}f}, {price=:,.{_p_}f}): ▶ {Bcolors.FAIL}the trading margin is insufficient error: ERR-201{Bcolors.ENDC} ")
                        self.debug.sound_alert(2)  
                        break       # exit while true loop => error return         
                    elif status == 8 and msg_code == 'ERR-888': # get_price() time out error ?
                        self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_buy_order{Bcolors.ENDC}({buy_qty=:,.{_q_}f}, {price=:,.{_p_}f})): ▶ {Bcolors.FAIL}get_price() time out error: {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                        break       # exit while true loop => error return 
                    else: # misc error
                        self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_buy_order{Bcolors.ENDC}({buy_qty=:,.{_q_}f}, {price=:,.{_p_}f})): ▶ {Bcolors.FAIL}misc error: {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                        break       # exit while true loop => error return 
                # end of if status != 0:
                             
                ### no error : res_df.shape[0] >= 1 (single row or multiple rows)            
                       
                ################################################################################################################
                ### 【3】ADD A BUY ORDER CHILD
                ################################################################################################################
    
                # res_df = get_price() response for exe_buy_order_market_wait()
                # ---------------------------------------------------------
                #  #   Column       Dtype              
                # ---------------------------------------------------------              
                #  0   executionId  object             
                #  1   fee          float64            
                #  2   lossGain     float64            
                #  3   orderId      object             
                #  4   positionId   object  ★           
                #  5   price        float64            
                #  6   settleType   object  (OPEN or CLOSE)             
                #  7   side         object  (BUY or SELL)           
                #  8   size         float64            
                #  9   symbol       object  (BTC_JPY)           
                #  10  timestamp    datetime64[ns, UTC]
                # --------------------------------------------------------- 
    
                ### write a buy order child
          
                # define buy order child list
                buy_order_id_list = []
                position_id_list = []
                real_qty_list = []
                real_price_list = []
                real_fee_list = []   
    
                # response df for exe_buy_order_market_wait(FAK) 
                for k in range(len(res_df)):
                    buy_order_id    = res_df.loc[k, 'orderId']          # buy order id (same order id)
                    position_id     = res_df.loc[k, 'positionId']       # position id (unique id)
                    real_qty        = res_df.loc[k, 'size']             # real qty   ★ 
                    real_price      = res_df.loc[k, 'price']            # real price ★
                    real_fee        = res_df.loc[k, 'fee']              # real fee
    
                    buy_order_id_list.append(buy_order_id)              # B9999-999999-999999
                    position_id_list.append(position_id)                # P9999-999999-999999
                    real_qty_list.append(real_qty)                      # 10, 10, 20 = 40
                    real_price_list.append(real_price)                  # XRP_JPY(64.470)
                    real_fee_list.append(real_fee)                      # zero(0)
    
                    # write_buy_order_child(buy_time: utcnow, buy_order_id: str, sell_time: utcnow, sell_order_id: str, position_id: str , qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:        
                    _sell_time_ = buy_time; _sell_order_id_ = 'Dummy';  _qty_ = real_qty; _price_ = real_price  
                    self.csv.write_buy_order_child(buy_time, buy_order_id, _sell_time_, _sell_order_id_, position_id, _qty_, _price_, real_qty, real_price, real_fee)
                # end of for k in range(len(res_df)): 
    
                first_buy_order_id = buy_order_id_list[0]   # same order ids              
                position_ids_by_semicolon = ''  # 'positionId1;positionId2;positionId3'
                for k, position_id_x in enumerate(position_id_list):
                    if k == 0:
                        position_ids_by_semicolon += position_id_x
                    else:
                        position_ids_by_semicolon += ';' + position_id_x    
    
                sub_sum_real_qty = round(sum(real_qty_list), _q_)   # XRP_JP(9999)               
                sub_sum_real_fee = round(sum(real_fee_list), 0)     # zero(0)
    
                if sum(real_price_list) > 0:
                    sub_mean_real_price = round(statistics.mean(real_price_list), _p_)  # XRP_JPY(99.123)
                else:
                    sub_mean_real_price = 0                      
    
                header_real_qty_list.append(sub_sum_real_qty)        
                header_real_price_list.append(sub_mean_real_price)
                header_real_fee_list.append(sub_sum_real_fee)                   
    
                ###################################################################################################################
                ### 【4】ADD A BUY ORDER SUB (buy_time, sell_time,...) ★ sell_time <= buy_time
                ###################################################################################################################
    
                ### write a buy order sub 
                # write_buy_order_sub(buy_time: utcnow, sell_time: utcnow, buy_order_id: str, sell_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:
                _sell_time_ = buy_time; _sell_order_id_ = 'Dummy';  _qty_ = sub_sum_real_qty # positionId = 'positionId1;positionId2;positionId3'
                self.csv.write_buy_order_sub(buy_time, _sell_time_, first_buy_order_id, _sell_order_id_, position_ids_by_semicolon, _qty_, price, sub_sum_real_qty, sub_mean_real_price, sub_sum_real_fee) # ★
    
                ### check exit condition     
    
                header_sum_real_qty = round(sum(header_real_qty_list), _q_)     # XRP_JPY(9999)           
                # self.debug.trace_write(f".... {self.gvar.callers_method_name} while({loop_count}): buy_qty={buy_qty}, header_sum_real_qty={header_sum_real_qty} ")            
                   
                # no outstanding buy_qty ?
                if round(qty, _q_) == round(header_sum_real_qty, _q_):  
                    # increment buy count      
                    self.gvar.buy_count += 1  
                    self.coin.buy_count += 1                  
                    break   # exit while true loop             
                else:       # outstanding buy_qty exists      
                    pass    # skip (do nothing)                      
                # end of if round(buy_qty, _q_) == round(header_sum_real_qty, _q_):  
    
                ### partial exe_buy_order_market_wait() was executed => continue to buy remaining qty
                    
                remaining_qty = buy_qty - header_sum_real_qty   # calculate new buy qty        
    
                buy_qty = remaining_qty # update new buy qty      
    
                # continue exe_buy_order_market_wait() ★           
            # end of while True:    
    
            #################################################################################
            ### 【5】ADD A BUY ORDER HEADER (buy_time, sell_time) ★ sell_time <= buy_time
            #################################################################################
    
            ### update buy order header(real_qty, real_price, real_fee)
            header_sum_real_qty = round(sum(header_real_qty_list), _q_)     # XRP_JPY(9999)    
            header_sum_real_fee = round(sum(header_real_fee_list), 0)       # zero(0)        
    
            if sum(header_real_price_list) > 0:
                header_mean_real_price = round(statistics.mean(header_real_price_list), _p_)  # XRP_JPY(99.123)
            else:
                header_mean_real_price = 0            
    
            ### write a buy order  
            # write_buy_order(buy_time: utcnow, sell_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:       
            _sell_time_ = buy_time  
            self.csv.write_buy_order(buy_time, _sell_time_, qty, price, header_sum_real_qty, header_mean_real_price, header_sum_real_fee)       
             
            self.debug.trace_write(f".... {self.gvar.callers_method_name} create_buy_order() ▶ {status=} normal return... ")                
    
            return status

    click image to zoom!
    図4-1
    図4-1はデモプログラムの実行結果です。 ここでは関数「auto_trade()」からBuySellクラスのbuy_sell(althorithm_id=0)メソッドを呼び出しています。 buy_sell()メソッドの引数に「althorithm_id=0」を指定しているので「アルゴリズム0」が適用されて買い・売りの取引を行います。 買いのタイミングになるとBuySellクラスのcreate_buy_order()メソッドを呼び出して成行きの買い注文を入れます。


    click image to zoom!
    図4-2
    図4-2はcreate_buy_order()で作成したCSVファイルをExcelで開いた画像です。 「buy_order(XRP_JPY).csv」ファイルには「buy_time, qty, price, real_qty, real_price,...」などが格納されています。 「real_qty, real_price」には約定したときの数量と価格(レート)が格納されます。 ここでは数量(10XRP_JPY)を注文しています。

    「buy_order_sub(XRP_JPY).csv」ファイルには「buy_time, buy_order_id, position_id, qty, price, real_qty, real_price,...」などが格納されています。 「position_id」のポジションIDは売り注文(買いポジションの決済)を入れるときに使います。買い注文は成行きなので通常すぐに約定します。

    「but_order_child(XRP_JPY).csv」ファイルには「buy_time, buy_order_id, position_id, qty, price, reqal_qty, real_price,...」などが格納されています。 買い注文のCSVファイルがなぜ3段階に階層化されているかと言えば、注文が約定されるとき数量が分割されるからです。 たとえば、数量「100XRP_JPY」を成行きで注文すると「20, 30, 50」のように分割されて約定されることがあります。 さらに、成行き注文のときは(FAK: Fill and Kill)が適用されて全数約定しないことがあります。 このような場合、create_buy_order()は残数の成行き注文を再度入れて全数約定するようにしています。

    たとえば、数量「100」を成行きで注文して「70」だけ約定したときは、再度「30」で成行き注文を入れます。 この場合、1回の買い注文で2個の注文IDが生成されることになります。 こういった理由で、買い注文のCSVファイルは3階層になっています。 ちなみに、売り注文のCSVファイルも同様に3階層になっています。


  5. BuySellクラスにcreate_sell_order()メソッドを追加する

    ここではBuySellクラスにcreate_sell_order()メソッドを追加します。 このメソッドは前出のcreate_buy_order()で買った仮想通貨を決済するために、 指値で売り注文を入れます。 指値の場合、成行きの注文と異なり「Fill and Store」が適用されます。 したがって、成行きのように途中で注文が終了することはありません。

    buy_sell.py:
        #################################################################################################################    
        def create_sell_order(self, sell_time: dt.datetime.utcnow, ms_price: float, buy_time: dt.datetime.utcnow) -> int:   # return status : ms_price (latest market sell price)   
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal                
    
            self.gvar.callers_method_name = 'create_sell_order():'
            self.debug.trace_write(f".... {self.gvar.callers_method_name} create_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, {ms_price=:,.{_p_}f}, {buy_time=:%Y-%m-%d %H:%M:%S}) ") 
    
            status = 0    
            
            ################################################
            ### 【1】CHECK DUPLICATE SELL ORDER 
            ################################################
    
            ### check duplicate sell order 
    
            # get the sell order : sell_order(BTC_JPY).csv
            sell_df = self.csv.get_sell_order()          
    
            # find a sell order 
            find_mask = sell_df['sell_time'] == sell_time   # PK(sell_time)
            dfx = sell_df[find_mask]          
            # duplicate sell order ?     
            if dfx.shape[0] > 0:
                status = 9      # set internal status code
                self.debug.trace_write(f".... {self.gvar.callers_method_name} duplicate sell order ({sell_time=:%Y-%m-%d %H:%M:%S}) ▶ {status=} error return... ")
                return status   # error return 
              
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv          
            buy_sub_df = self.csv.get_buy_order_sub()  
    
            ### OPEN (load the buy order child csv file ) : buy_order_child(BTC_JPY).csv          
            buy_ch_df = self.csv.get_buy_order_child()  
       
            ######################################################################
            ### 【2】READ ALL BUY ORDER SUBs (filter by buy_time)
            ######################################################################        
            
            # define sell order sub list
            sub_qty_list = []
            sub_price_list = []
    
            # filter the buy order sub by buy_time
            filter_mask = buy_sub_df['buy_time'] == buy_time
            buy_subx_df = buy_sub_df[filter_mask]
            if buy_subx_df.shape[0] > 0:
                buy_subx_df.reset_index(inplace=True)
    
            # get all buy order sub filtered by buy_time 
            for i in range(len(buy_subx_df)):   # ★ buy_subx_df ★
                ### get buy order sub info
                sub_buy_time        = buy_subx_df.loc[i, 'buy_time']                         
                sub_buy_order_id    = buy_subx_df.loc[i, 'buy_order_id']      
                sub_qty             = buy_subx_df.loc[i, 'qty']                                    
                sub_price           = buy_subx_df.loc[i, 'price']               
                sub_real_qty        = buy_subx_df.loc[i, 'real_qty']                            
                sub_real_price      = buy_subx_df.loc[i, 'real_price']            
                sub_real_fee        = buy_subx_df.loc[i, 'real_fee']                  
                      
                #################################################################################
                ### 【3】READ ALL BUY ORDER CHILD (filter by buy_time + buy_order_id)
                #################################################################################  
    
                # define sell order child list
                ch_buy_order_id_list = []
                ch_sell_order_id_list = []
                ch_position_id_list = []
                ch_qty_list = []
                ch_price_list = []                  
    
                ### filter the buy order child by buy_time + buy_order_id
                
                # buy order child is empty ?
                if buy_ch_df.shape[0] == 0:            
                    buy_chx_df = pd.DataFrame()
                else: # filter buy order child    
                    filter_mask = (buy_ch_df['buy_time'] == buy_time) & (buy_ch_df['buy_order_id'] == sub_buy_order_id)
                    buy_chx_df = buy_ch_df[filter_mask]
                    if buy_chx_df.shape[0] > 0:
                        buy_chx_df.reset_index(inplace=True)            
    
                # get all buy order child filtered by buy_time + buy_order_id
                for j in range(len(buy_chx_df)):   # ★ buy_chx_df ★
                    ### get buy order child info
                    ch_buy_time     = buy_chx_df.loc[j, 'buy_time']            
                    ch_buy_order_id = buy_chx_df.loc[j, 'buy_order_id']       
                    ch_position_id  = buy_chx_df.loc[j, 'position_id']  
                    ch_qty          = buy_chx_df.loc[j, 'qty']                                    
                    ch_price        = buy_chx_df.loc[j, 'price']               
                    ch_real_qty     = buy_chx_df.loc[j, 'real_qty']                            
                    ch_real_price   = buy_chx_df.loc[j, 'real_price']            
                    ch_real_fee     = buy_chx_df.loc[j, 'real_fee'] 
    
                    #############################################################################################
                    ### 【4】EXECUTE SELL CLOSE ORDER LIMIT (FAS:Fill And Store) FOR EACH BUY ORDER CHILD 
                    ############################################################################################# 
           
                    sell_qty = ch_real_qty
                    sell_price = self.trade.get_sell_price(ch_real_price, trace=True)                          
    
                    # market price is greater than sell price
                    if ms_price > sell_price:
                        sell_price = ms_price                  
                  
                    ### execute a sell close order with limit(FAS) 
                    # exe_sell_close_order_limit(position_id: str, qty: float, price: float) -> float:
                    status, res_sell_order_id, msg_code, msg_str = self.api.exe_sell_close_order_limit(ch_position_id, sell_qty, sell_price)       
                    # res_sell_order_id => 'S9999-999999-999999'       
    
                    # exe_sell_close_order_limit() error ?
                    if status != 0:
                        if status == 1 and msg_code == 'ERR-208':   # Exceeds the available balance
                            self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_sell_order{Bcolors.ENDC}({sell_qty=:,.{_q_}f}): ▶ {Bcolors.FAIL}the available balance exceeded ERR-208: continue...{Bcolors.ENDC} ")
                            self.debug.sound_alert(2)  
                            continue    # continue to next buy order child        
                        else: # misc error
                            self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_sell_order{Bcolors.ENDC}({sell_qty=:,.{_q_}f}): ▶ {Bcolors.FAIL}misc error: {status=}, {msg_code} - {msg_str} {Bcolors.ENDC} ")                               
                            self.debug.sound_alert(2) 
                            continue    # continue to next buy order child             
                    # end of if status != 0:
    
                    ### no error for exe_sell_close_order_limit()        
    
                    ################################################################################################################
                    ### 【5】ADD A SELL ORDER SUB
                    ################################################################################################################
                            
                    ### write a sell order sub
                    # write_sell_order_sub(sell_time: utcnow, buy_time: utcnow, sell_order_id: str, buy_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:                                                         
                    self.csv.write_sell_order_sub(sell_time, buy_time, res_sell_order_id, ch_buy_order_id, ch_position_id, sell_qty, sell_price)       
     
                    ch_buy_order_id_list.append(ch_buy_order_id)
                    ch_sell_order_id_list.append(res_sell_order_id)
                    ch_position_id_list.append(ch_position_id)
                    ch_qty_list.append(sell_qty)
                    ch_price_list.append(sell_price)
    
                    # continue to next buy order child
                # end of for j in range(len(buy_chx_df)):    
    
                ch_sum_qty = round(sum(ch_qty_list), _q_)                                                 
                if sum(ch_price_list) > 0:
                    ch_mean_price = round(statistics.mean(ch_price_list), _p_) 
                else:
                    ch_mean_price = 0    
    
                sub_qty_list.append(ch_sum_qty)
                sub_price_list.append(ch_mean_price)                             
    
                #####################################################################################
                ### 【6】UPDATE BUY ORDER SUB (sell_order_id, sell_time)
                #####################################################################################          
    
                sell_order_ids_by_semicolon = ''    # 'SellOrderID1;SellOrderID2;SellOrderID3'
                for k, sell_order_id_x in enumerate(ch_sell_order_id_list):
                    if k == 0:
                        sell_order_ids_by_semicolon += sell_order_id_x
                    else:
                        sell_order_ids_by_semicolon += ';' + sell_order_id_x    
    
                ### update buy order sub (sell_order_id, sell_time) 
                find_mask = (buy_sub_df['buy_time'] == buy_time) & (buy_sub_df['buy_order_id'] == sub_buy_order_id)
                dfx = buy_sub_df[find_mask]
                # not found buy order sub ?
                if dfx.shape[0] == 0:
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_sell_order{Bcolors.ENDC}(): {Bcolors.FAIL} buy order sub not found ▶ {sub_buy_order_id=} {Bcolors.ENDC} ")
                else: # found buy order sub => update sell_order_id, sell_time 
                    buy_sub_df.loc[find_mask, 'sell_order_id'] = sell_order_ids_by_semicolon # 'SellOrderID1;SellOrderID2;SellOrderID3'  
                    buy_sub_df.loc[find_mask, 'sell_time'] = sell_time
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} update_buy_order_sub({buy_time=:%Y-%m-%d %H:%M:%S}, {sub_buy_order_id=}, {sell_order_ids_by_semicolon=}, {sell_time=:%Y-%m-%d %H:%M:%S}) ")  
                
                # continue to next buy order sub           
            # end of for i in range(len(buy_subx_df)):          
    
            #################################################################################
            ### 【7】SAVE BUY ORDER SUB TO CSV FILE
            #################################################################################
    
            ### CLOSE (save the buy order sub csv file) : buy_order_sub(BTC_JPY).csv     
            csv_file = self.folder_trading_type + f'buy_order_sub({symbol}).csv' 
            # desktop-pc/bot/buy_sell/buy_order_sub(BTC_JPY).csv  
            buy_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode                                     
              
            ################################################################################
            ### 【8】ADD A SELL ORDER 
            ################################################################################            
    
            ### update sell order (real_qty, real_price, real_fee) 
            sub_sum_qty = round(sum(sub_qty_list), _q_)                                    
            
            if sum(sub_price_list) > 0:
                sub_mean_price = round(statistics.mean(sub_price_list), _p_) 
            else:
                sub_mean_price = 0    
    
            # write_sell_order(sell_time: utcnow, buy_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:    
            self.csv.write_sell_order(sell_time, buy_time, sub_sum_qty, sub_mean_price)  
    
            ################################################################################
            ### 【9】UPDATE A BUY ORDER (sell_time)
            ################################################################################          
            # update_buy_order(buy_time: utcnow, col_name_list: list, col_value_list: list) -> None:    # PK(buy_time)
            self.csv.update_buy_order(buy_time, col_name_list=['sell_time'], col_value_list=[sell_time])    
    
            self.debug.trace_write(f".... {self.gvar.callers_method_name} create_sell_order() ▶ {status=} normal return... ")
    
            return status

    click image to zoom!
    図5-1
    図5-1はデモプログラムの実行結果です。 ここでは関数「auto_trade()」からBuySellクラスのbuy_sell(althorithm_id=0)メソッドを呼び出しています。 buy_sell()メソッドの引数に「althorithm_id=0」を指定しているの「アルゴリズム0」が適用されて買い・売りの取引を行います。

    売りのタイミングになるとBuySellクラスのcreate_sell_order()メソッドを呼び出して指値の売り注文(買いポジションの決済)を入れます。 つまり、前出のcreate_buy_order()メソッドで注文した買いポジションを決済しています。 画面に表示されているログから分かるように内部的には、 Apiクラスのexe_sell_close_order_limit()メソッドを呼び出して指値の売り注文を入れています。


    click image to zoom!
    図5-2
    図5-2はcreate_sell_order()メソッドで作成した売り注文のCSVファイルをExcelで開いた画像です。 「sell_order(XRP_JPY).csv」ファイルには「sell_time, qty, price, real_qty, real_price,...」などが格納されています。 「qty, price」は指値で売り注文を入れたときの数量と価格です。 「real_qty, real_price」は注文が約定したときの数量、価格(レート)です。 この状態ではまだ約定していないので双方とも「0」になっています。

    「sell_order_sub(XRP_JPY).csv」ファイルには「sell_time, sell_order_id, position_id, qty, price, real_qty, real_price, buy_order_id,..」 などが格納されています。 「real_qty, real_price」には約定したときの数量と価格(レート)が格納されます。 このファイルには対応する買い注文の注文ID(buy_order_id)も格納されます。

    実際に約定したときは、注文数が分割されることがあるので「sell_order_child(XRP_JPY).csv」ファイルを作成します。


  6. BuySellクラスにupdate_order(), update_closed_order(), update_partially_closed_order()メソッドを追加する

    ここではBuySellクラスにupdate_order()、update_partially_closed_order()メソッドを追加します。 update_order()は親メソッドで、update_partially_closed_order()は子メソッドです。 update_order()からは、 update_closed_order(), update_partially_closed_order(), update_order_profit(), stop_loss(), cancel_pending_order(), close_pending_order()などのメソッドも呼びます。

    update_partially_closed_order()では、 買いと売りの取引が完了した注文のCSVファイルを更新して保存します。 具体的には約定したときの数量(執行数量)、金額(レート)等を更新します。

    buy_sell.py:
        ###############################
        def update_order(self) -> None:
            """
            
            """
            ########################################################################
            ### 【1】UPDATE CLOSED SELL ORDER 
            ########################################################################
            ### update closed sell order
            self.update_closed_order()   
    
            ########################################################################
            ### 【2】UPDATE ORDER PROFIT (PART) : first_pass(True)
            ########################################################################
            ### update order profit 
            self.update_order_profit(first_pass=True)           
    
            ########################################################################
            ### 【3】CHECK STOP LOSS : gvar.stoploss_method_id : 0, 1,...
            ########################################################################
            ### check latest price(GMO) and stop loss price 
            if self.gvar.stop_loss:    
                self.stop_loss(self.gvar.stoploss_method_id)    # 0, 1,...  
           
            ########################################################################
            ### 【4】CANCEL PENDING ORDERS OR CLOSE PENDING ORDERS 
            ########################################################################
            ### cancel pending orders
            # close pending orders request & cancel pending order ? => call cancel_pending_orders() method       
            if self.gvar.cancel_pending_orders and self.gvar.close_pending_orders_request:         
                self.cancel_pending_order() 
            elif self.coin.cancel_pending_orders: # cancel pending order by coin (one time request)            
                self.cancel_pending_order()             
            elif self.gvar.close_pending_orders: # close pending orders (current time is between 05:00AM - 06:00AM)  
                self.close_pending_order()                          
    
            ########################################################################
            ### 【5】 UPDATE ORDER PROFIT (PART2) : first_pass(False)
            ########################################################################
            ### update the order file ( order(BTC).csv )
            if self.gvar.stop_loss or self.gvar.cancel_pending_orders:
                self.update_order_profit(first_pass=False)  
            elif self.coin.cancel_pending_orders: # cancel pending order by coin (one time request)            
                self.update_order_profit(first_pass=False)    
            elif self.gvar.close_pending_orders: # close pending orders (current time is between 05:00AM - 06:00AM)      
                self.update_order_profit(first_pass=False) 
    
            return
        
        ###################################### 
        def update_closed_order(self) -> None:  
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal   
            self.gvar.callers_method_name = 'update_closed_order():'
            order_completed = self.trade.get_order_status(exclude=False)   # include stop_loss, cancelled, closed order info
            
            df = self.coin.master2_df    
            mas_df = pd.DataFrame(df)   # cast df (master dataframe)
            self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}last order completed({order_completed}){Bcolors.ENDC}, mas_df.shape({mas_df.shape[0]}), close({self.gvar.close_pending_orders_request}), cancel({self.gvar.cancel_pending_orders}) ")       
    
            # mas_df is empty ? => SYTEM ERROR
            if mas_df.shape[0] == 0: 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - mas_df({mas_df.shape[0]}) is empty. skip update_order() ")
                self.debug.sound_alert(3)                  
                return 
    
            ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)       
    
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
    
            # get_orderbooks_try() -> tuple:  # return (sell_df, buy_df)   
            sell_bk, buy_bk = self.api.get_orderbooks_try()
            # sleep(0.5)
            sell_bk_df = pd.DataFrame(sell_bk)  # ascending order of price [-1, price]      : low to high price 100, 110, 120, 130, 140, 150
            buy_bk_df = pd.DataFrame(buy_bk)    # descending order of price iloc[0, price]  : high to low price 150, 140, 130, 120, 110, 100
            
            # not empty order books ?
            if sell_bk_df.shape[0] > 0 and buy_bk_df.shape[0] > 0:                
                # get latest sell/buy price from the order books
                bk_sell_price = sell_bk_df.iloc[0]['price'] # get lowest sell price : 100
                bk_buy_price = buy_bk_df.iloc[0]['price']   # get highest buy price : 150
            else:
                bk_sell_price = 0.0
                bk_buy_price = 0.0
    
            # incomplete sell order (pending sell order) ?
            if not order_completed: 
                ### print GMO orderbooks info (sell price, buy price)
    
                # # get_orderbooks_try() -> tuple:  # return (sell_df, buy_df)   
                # sell_bk, buy_bk = self.api.get_orderbooks_try()
                # sleep(0.5)
                # sell_bk_df = pd.DataFrame(sell_bk)  # ascending order of price [-1, price]      : low to high price 100, 110, 120, 130, 140, 150
                # buy_bk_df = pd.DataFrame(buy_bk)    # descending order of price iloc[0, price]  : high to low price 150, 140, 130, 120, 110, 100
                
                # not empty order books ?
                if sell_bk_df.shape[0] > 0 and buy_bk_df.shape[0] > 0:                
                    # get latest sell/buy price from the order books
                    bk_sell_price = sell_bk_df.iloc[0]['price'] # get lowest sell price : 100
                    bk_buy_price = buy_bk_df.iloc[0]['price']   # get highest buy price : 150
    
                    # get the last sell price from the sell order sub 
                    last_sell_price = sell_sub_df.iloc[-1]['price'] # 120
    
                    # get sell position number (buy => sell)
                    filter_mask = sell_bk_df['price'] < last_sell_price # < 120
                    dfx = sell_bk_df[filter_mask]   # 100, 110
                    sell_position = dfx.shape[0]    # 2
    
                    # update sell position count
                    if sell_position >= 91:
                        self.coin.position_count_91_999 += 1
                    elif sell_position >= 71:
                        self.coin.position_count_71_90 += 1
                    elif sell_position >= 51:
                        self.coin.position_count_51_70 += 1
                    elif sell_position >= 31:
                        self.coin.position_count_31_50 += 1
                    elif sell_position >= 11:
                        self.coin.position_count_11_30 += 1
                    else:
                        self.coin.position_count_0_10 += 1                     
    
                    self.coin.sell_position_list.append(sell_position)  # append sell_position : coin.sell_position=[10, 8, 5, 3, 1, 0]       
                    self.debug.trace_write(f".... {self.gvar.callers_method_name}{Bcolors.WARNING} {sell_position=}{Bcolors.ENDC} => close condition [{last_sell_price=:,.{_p_}f} <= {bk_buy_price=:,.{_p_}f}] ")   # 120 <= 150                                  
    
                    reverse_sell_position_list = self.coin.sell_position_list[::-1] # [0, 1, 3, 5, 8, 10]      
                    if len(reverse_sell_position_list) < 21:
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {reverse_sell_position_list=} ")            
                    else:
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {reverse_sell_position_list[0:21]=} ")     
                else: # empty order books
                    self.coin.sell_position_list = []   # reset sell position list
            # end of if not order_completed: 
    
            #################################################
            ### 【1】READ ALL SELL ORDER 
            #################################################
    
            ### get all sell order 
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']   # unique time                  
                buy_time        = sell_df.loc[i, 'buy_time']    # unique time                        
                sell_qty        = sell_df.loc[i, 'qty']         
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']    
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True        
                     
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell order sub
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell order sub ?
                    if dfx.shape[0] > 0:
                        dfx.reset_index(inplace=True)                
                        sell_order_id = str(dfx.loc[0, 'sell_order_id'])    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sell_order_id})... ")                        
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_order_id}) has been closed: sell_order_closed={sell_order_closed} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_time:%Y-%m-%d %H:%M:%S}) has been closed: sell_order_closed={sell_order_closed} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # continue to next sell order     
                # end of if sell_order_closed:    
    
                ### open or incomplete sell order
    
                ##########################################################################################
                ### 【2】READ ALL SELL ORDER SUB & EXECUTE GET_PRICE() FOR EACH SELL ORDER SUB
                ##########################################################################################
    
                ### for sell order sub : use pandas dataframe groupby()     
     
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order : 2 rows
                for j in range(len(sell_subx_df)):   # ★ sell_subx_df ★
                    sell_order_sub_seq = j + 1 # 1, 2, 3,....
    
                    ### get sell order sub info
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # S9999-999999-999999 ★ (unique id) used for get_price() api
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # B9999-999999-999999 ★ (non unique id)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # (unique id)
                    sub_sell_qty        = sell_subx_df.loc[j, 'qty']                # 10
                    sub_sell_price      = sell_subx_df.loc[j, 'price']            
                    sub_sell_real_qty   = sell_subx_df.loc[j, 'real_qty']           # 10
                    sub_sell_real_price = sell_subx_df.loc[j, 'real_price']            
                    sub_sell_real_fee   = sell_subx_df.loc[j, 'real_fee']  
                    sub_sell_stop_loss  = sell_subx_df.loc[j, 'stop_loss'] 
                    sub_sell_cancelled  = sell_subx_df.loc[j, 'cancelled']        
    
                    sub_sell_order_open = False
                    sub_sell_order_closed = False
                    sub_sell_order_incomplete = False
    
                    if sub_sell_real_qty == 0:
                        sub_sell_order_open = True
                    elif sub_sell_qty == sub_sell_real_qty:
                        sub_sell_order_closed = True
                    else:
                        sub_sell_order_incomplete = True                             
    
                    # fake sell sub order ? => SKIP
                    if sub_sell_order_id.startswith('Fake'): 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id})... ") 
                        continue             
                        
                    # cancelled sell sub order ? => SKIP
                    if sub_sell_cancelled:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled ▶ sub_sell_cancelled={sub_sell_cancelled} ")  
                        continue
    
                    # stop loss sell sub order ? => SKIP
                    if sub_sell_stop_loss:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss ▶ sub_sell_stop_loss={sub_sell_stop_loss} ")  
                        continue            
    
                    # closed sell sub order ? => SKIP 
                    if sub_sell_order_closed:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed ▶ sub_sell_order_closed={sub_sell_order_closed} ")  
                        continue     
    
                    ### open or incomplete sell order sub
    
                    ############################################################
                    ### 【3】UPDATE SELL ORDER SUB (get_price_count+1)
                    ############################################################
    
                    ### update sell order sub (get_price_count += 1)  ★         
                    # DO NOT USE: self.csv.update_sell_order_sub_count(sell_time, sell_order_id)        
                    find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                    dfx = sell_sub_df[find_mask]
                    # not found sell order sub ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub_count({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found: {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order sub      
                        sell_sub_df.loc[find_mask, 'get_price_count'] += 1   # increment get_price_count by 1
                        ix = dfx.index[0]
                        sub_sell_get_price_count = sell_sub_df.loc[ix, 'get_price_count']
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub_count({sub_sell_order_id=}, {sub_sell_get_price_count=})  ")   
                    # end of if dfx.shape[0] == 0:                               
    
                    ### get order_status
                    qty = sub_sell_qty
                    price = sub_sell_price
    
                    # get_order_status_try(order_id: str, qty: float, price: float) ->tuple:    # return (status, order_status)                  
                    status, order_status = self.api.get_order_status_try(sub_sell_order_id, qty, price)     # ★ qty, price are used for fake mode only 
                    # status: 0(ok), 4(invalid request), 8(retry error), 9(internal error), ?(misc error) 
    
                    ### status = 0; order_status = 'EXPIRED'    # DEBUG DEBUG DEBUG
    
                    # get_order_status() error ?
                    if status != 0:                       
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}) error ▶ {Bcolors.FAIL} {status=}, {order_status=} {Bcolors.ENDC} ")                               
                        continue    # continue to next sell order sub                       
    
                    ### get_order_status() : status == 0, order_status in 'WAITING,MODIFYING,CANCELLING,ORDERED,EXECUTED,EXPIRED,CANCELED'
    
                    # pending exe_sell_close_order_limit() request ? : ORDERED(指値注文有効中), EXECUTE(全量約定)
                    if order_status in 'WAITING,MODIFYING,CANCELLING,ORDERED':                               
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}get_order_status_try({sub_sell_order_id}, {qty:,.{_q_}f}, {price:,.{_p_}f}){Bcolors.ENDC} ▶ {order_status=} pending exe_sell_close_order_limit() request ")                         
                        continue    # continue to next sell order sub                  
            
                    ### executed exe_sell_close_order_limit() : order_status in 'EXECUTED(全量約定),EXPIRED(全量失効|一部失効),CANCELED'       
    
                    if order_status in 'EXPIRED,CANCELED':                      
                        ### update sell order, sell order sub and order csv file
                        if order_status == 'CANCELED':
                             self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}): {Bcolors.FAIL}the order has been cancelled by GMO. please confirm the reason{Bcolors.ENDC} ")   
                             self.debug.sound_alert(3)
                             continue    # continue to next sell order sub 
    
                        if order_status == 'EXPIRED':     
                            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}): {Bcolors.FAIL}the order has been expired due to loss cut by GMO{Bcolors.ENDC} ")   
                            self.debug.sound_alert(3)
    
                            ### close sell order sub
    
                            # update a sell order sub (update real_qty, real_price, stop_loss)                                             
                            # DO NOT USE: self.csv.update_sell_order_sub(sell_time, sub_order_id, symbol, col_name_list, col_value_list) 
                            find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                            dfx = sell_sub_df[find_mask]
                            # not found sell order sub ?
                            if dfx.shape[0] == 0:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found: {dfx.shape=}{Bcolors.ENDC} ")   
                            else: # found sell order sub => update sell order sub (real_qty, real_price, real_fee, stop_loss) 
                                sell_sub_df.loc[find_mask, 'real_qty']   = sub_sell_qty  
                                sell_sub_df.loc[find_mask, 'real_price'] = 9_999_999.0  # gmo loss cut rate 
                                sell_sub_df.loc[find_mask, 'real_fee']   = 0.0 
                                sell_sub_df.loc[find_mask, 'stop_loss'] = True
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub({sub_sell_order_id=}, real_qty={sub_sell_qty:,.{_q_}f}, real_price={9_999_999.0:,.{_p_}f}, real_fee={0.0:,.0f}, stop_loss=True) ")   
                            # end of if dfx.shape[0] == 0:  
    
                            ### close sell order      
    
                            # update sell order (real_qty, real_price, real_fee) 
                            find_mask = sell_df['sell_time'] == sell_time
                            dfx = sell_df[find_mask]
                            # not found sell order ?
                            if dfx.shape[0] == 0:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL} ({sell_time=:%Y-%m-%d %H:%M:%S}) not found: {dfx.shape=}{Bcolors.ENDC} ")   
                            else: # found sell order 
                                sell_df.loc[find_mask, 'real_qty']   = sell_qty                 
                                sell_df.loc[find_mask, 'real_price'] = 9_999_999.0    # gmo loss cut rate 
                                sell_df.loc[find_mask, 'real_fee']   = 0.0 
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={sell_qty:,.{_q_}f}, real_price={9_999_999.0:,.{_p_}f}, real_fee={0.0:,.0f}) ")                                                    
                            # end of if dfx.shape[0] == 0:                                                      
    
                            ### update order (update stop_loss)                
               
                            # update_order_csv_buy(buy_time: utcnow, buy_order_id: str, col_name_list: list, col_value_list: list) -> None: # PK(buy_time + buy_order_id)
                            self.csv.update_order_csv_buy(sub_buy_time, sub_buy_order_id, col_name_list=['stop_loss'], col_value_list=[True])                            
    
                            continue    # continue to next sell order sub                  
                        # end of if order_status == 'EXPIRED':     
                    # end of if order_status in 'EXPIRED,CANCELED':                
    
    
                    ### get highest buy price from buy orderbooks
                    latest_book_price = bk_buy_price    # bk_buy_price = buy_bk_df.iloc[0]['price']    
    
                    # ### get real qty, sell price, fee from the GMO
                    # ### get the latest close price from the GMO master              
                    # ### mas_latest_close_price = mas_df.iloc[-1]['close']   # get latest close price (GMO)              
    
                    _debug1_ = self.coin.debug_mode_debug1; _debug2_ = self.coin.debug_mode_debug2    
    
                    ######################################################################################### DEBUG(1), DEBUG(2)           
                    # _debug1_ = True; _debug2_ = True    # ★ TEMP TEMP TEMP ★   
                    # self.debug.trace_warn(f".... DEBUG _debug1_ = True; _debug2_ = True ")              
                    ######################################################################################### DEBUG(1), DEBUG(2)  
    
                    ### get sell real qty, price & fee from the GMO : get_price() for exe_sell_close_order_limit() ★     
                    
                    sleep(1)    # sleep 1 sec            
                    # get_price_try(order_id: str, qty: float, price: float, latest_book_price=0.0, debug1=False, debug2=False) -> tuple: 
                    status, res_df, msg_code, msg_str = self.api.get_price_try(sub_sell_order_id, sub_sell_qty, sub_sell_price, latest_book_price, _debug1_, _debug2_)  # ★ qty, price, close_price are used for fake mode                  
                    # status: 0(ok),  8(retry error), 9(empty dataframe or internal error), ?(misc error)
                    sleep(1)    # sleep 1 sec            
    
                    res_df = pd.DataFrame(res_df)   # cast dataframe
    
                    # not real mode and debug (stop loss or cancel pending orders) ?
                    ######################################################################################### DEBUG(1_2) DEBUG(1_2) DEBUG(1_2) : update_order()
                    if not self.coin.real_mode:
                        if self.coin.debug_mode_debug1: 
                            if res_df.shape[0] > 0:
                                # ★ create two sell order subs for each sell order header ★
                                # update_get_price_response_for_debug1_2(res_df: pd.DataFrame, sell_order_sub_seq: int) -> pd.DataFrame
                                res_df = self.trade.update_get_price_response_for_debug1_2(res_df, sell_order_sub_seq)  # 1, 2, 3, ...
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_get_price_response_for_debug1_2({sell_order_sub_seq=}) ▶ {res_df.shape=} ")  
                    ######################################################################################## DEBUG(1_2) DEBUG(1_2) DEBUG(1_2) : update_order()        
    
    
                    ####################################################################################### DEBUG(2) DEBUG(2) DEBUG(2) : update_order()
                    if not self.coin.real_mode:
                        if self.coin.debug_mode_debug2:
                            # ★ create outstanding sell order sub depending on get_price_count ★
                            # get sell 'get_price_count'
                            find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                            dfx = sell_sub_df[find_mask]
                            # found sell order sub ?
                            if dfx.shape[0] > 0:
                                ix = dfx.index[0]                                    
                                get_price_count = sell_sub_df.loc[ix, 'get_price_count'] # sell_sub_df.loc[find_mask, 'get_price_count'].values[0]   # 0,1,2,3,4,5,...
                                # update_get_price_response_for_debug2(res_df: pd.DataFrame, get_price_count: int) -> pd.DataFrame:
                                res_df = self.trade.update_get_price_response_for_debug2(res_df, get_price_count)                                
                                if res_df.shape[0] == 0:
                                    status = 9                          # set internal status code
                                    msg_code = 'ERR-999'                # null => ERR-999
                                    msg_str = 'DataFrame is empty'      # null => DataFrame is empty  
                                else:
                                    status = 0
                                    msg_code = 'OK'
                                    msg_str = 'Normal Return'                                                            
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_get_price_response_for_debug2({get_price_count=}) ▶ {status=}, {res_df.shape=} ")         
                    ###################################################################################### DEBUG(2) DEBUG(2) DEBUG(2) : update_order()
                 
                    # get_price() error ?
                    if status != 0:     
                        if status == 9: # pending exe_sell_close_order_limit() request ? 
                            # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}get_price({sub_sell_price:,.{_p_}f}){Bcolors.ENDC} ▶ {status=} pending exe_sell_close_order_limit() request: continue to next sell order sub... ") 
                            status = 0  # reset status code
                            continue    # continue to next sell order sub                  
                        else: # status == 1 'ERR-5008' Timestamp for this request is too late
                            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_price({sub_sell_price:,.{_p_}f}) ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str} {Bcolors.ENDC} ")                               
                            continue    # continue to next sell order sub 
                        # end of if status == 9: 
                    else:   # normal return => executed exe_sell_close_order_limit() request                
                        pass    # self.debug.sound_alert(1) 
                    # end of if status != 0: 
    
                    ### no error : res_df.shape[0] >= 1 (single or multiple rows)  
     
                    ####################################################################
                    ### 【4】UPDATE A SELL ORDER SUB (real_qty, real_price, real_fee) 
                    ####################################################################
    
                    # res_df = get_price() response for exe_sell_close_order_limit()         
                    # ----------------------------------------------------------------  
                    #  #   Column       Dtype              
                    # ----------------------------------------------------------------              
                    #  0   executionId  object             
                    #  1   fee          float64            
                    #  2   lossGain     float64            
                    #  3   orderId      object  (non unique id => same id)
                    #  4   positionId   object  (unique id)                     
                    #  5   price        float64            
                    #  6   settleType   object  (OPEN or CLOSE)             
                    #  7   side         object  (BUY or SELL)           
                    #  8   size         float64            
                    #  9   symbol       object  (BTC_JPY)           
                    # 10   timestamp    datetime64[ns, UTC]
                    # ----------------------------------------------------------------                  
    
                    ### update a sell order sub ★             
                    sub_sum_real_qty  = round(res_df.groupby('orderId')['size'].sum().values[0], _q_)           # round(999.12, _q_)
                    sub_sum_real_fee  = round(res_df.groupby('orderId')['fee'].sum().values[0], 0)              # zero(0) 
                    sub_mean_real_price = round(res_df.groupby('orderId')['price'].mean().values[0], _p_)       # round(999999.123, _p_)
                              
                    # DO NOT USED: self.csv.update_sell_order_sub(sell_time, sub_order_id, symbol, col_name_list, col_value_list) 
                    find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                    dfx = sell_sub_df[find_mask]
                    # not found sell order sub ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found: {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order sub => update sell order sub (real_qty, real_price, real_fee) 
                        sell_sub_df.loc[find_mask, 'real_qty']   = sub_sum_real_qty  
                        sell_sub_df.loc[find_mask, 'real_price'] = sub_mean_real_price 
                        sell_sub_df.loc[find_mask, 'real_fee']   = sub_sum_real_fee 
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub({sub_sell_order_id=}, real_qty={sub_sum_real_qty:,.{_q_}f}, real_price={sub_mean_real_price:,.{_p_}f}, real_fee={sub_sum_real_fee:,.0f}) ")   
                    # end of if dfx.shape[0] == 0:  
    
                    ##############################################################################################################################
                    ### 【5】ADD A SELL ORDER CHILD (sell_time, sell_order_id, real_qty, real_price, real_fee) + (buy_time, buy_order_id)
                    ##############################################################################################################################
                      
                    ### check exit condition : sub_sell_qty=sub_sell_qty, sub_sum_real_qty=sum(res_df['size'])          
    
                    # ######################################### TEMP TEMP TEMP
                    # temp_sell_qty = round(sub_sell_qty, _q_)
                    # temp_real_qty = round(sub_sum_real_qty, _q_)
                    # self.debug.trace_warn(f".... DEBUG: {temp_sell_qty=:,.{_q_}f}, {temp_real_qty=:,.{_q_}f} ")
                    # ######################################## TEMP TEMP TEMP        
    
                    # completed exe_sell_close_order_limit() request ?
                    if round(sub_sell_qty, _q_) == round(sub_sum_real_qty, _q_):                                                
                        # changed open => closed  
                        if self.coin.real_mode:                                   
                            self.debug.sound_alert(1)
    
                        # increment sell count
                        self.gvar.sell_count += 1  
                        self.coin.sell_count += 1        
           
                        # response df for get_price(): exe_sell_close_order_limit()
                        for k in range(len(res_df)):
                            sell_order_id   = res_df.loc[k, 'orderId']      # sell order id (unique id)
                            position_id     = res_df.loc[k, 'positionId']   # position id (non unique id => same id)
                            real_qty        = res_df.loc[k, 'size']         # real qty
                            real_price      = res_df.loc[k, 'price']        # real price
                            real_fee        = res_df.loc[k, 'fee']          # real fee         
    
                            if not self.coin.real_mode:
                                position_id = sub_position_id                        
            
                            ### write a sell order child ★      
                            # write_sell_order_child(sell_time: utcnow, sell_order_id: str, buy_time: utcnow, buy_order_id: str, position_id: str, qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:               
                            _qty_ = real_qty; _price_ = real_price
                            self.csv.write_sell_order_child(sub_sell_time, sell_order_id, sub_buy_time, sub_buy_order_id, position_id, _qty_, _price_, real_qty, real_price, real_fee)    
                        # end of for k in range(len(res_df)):                   
                                                                       
                        # ### update sell order sub (bought=True) ★ DO NOT USE update_sell_order_sub() : SUSPEND SUSPEND SUSPEND                
                                                                                                                     
                    else:   # outstanding sell_qty exists => incomplete status (sell order / sell order sub will be updated by update_order() 
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} incomplete sell order sub({sub_sell_order_id}) ▶ sell_qty={sub_sell_qty:,.{_q_}f}, sell_real_qty={sub_sum_real_qty:,.{_q_}f} ")    
                        # pass    # skip (do nothing)    
                    # end of if sub_sell_qty == total_real_qty:
    
                    # continue to next sell order sub
                # end of for j in range(len(sell_subx_df)):
    
                #################################################################################
                ### 【6】SAVE BUY/SELL ORDER SUB TO CSV FILES
                #################################################################################
    
                ### CLOSE (save the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
                csv_file = self.folder_trading_type + f'buy_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/buy_order_sub(BTC_JPY).csv  
                buy_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode   
                                        
                ### CLOSE (save the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
                csv_file = self.folder_trading_type + f'sell_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/sell_order_sub(BTC_JPY).csv  
                sell_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode               
    
                #################################################################################
                ### 【7】UPDATE SELL ORDER (real_qty, real_price, real_fee)
                #################################################################################     
    
                ### calculate total sell qty / fee and mean price ★ sell_time ★
                filter_mask = sell_sub_df['sell_time'] == sell_time
                dfx = sell_sub_df[filter_mask]    
                # not found sell order sub ?
                if dfx.shape[0] == 0:
                    self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL} sell order sub({sell_time:%Y-%m-%d %H:%M:%S}) not found: {dfx.shape=}{Bcolors.ENDC} ")   
                else: # found sell order sub                        
                    header_sum_real_qty = dfx.groupby('sell_time')['real_qty'].sum().values[0]    
                    header_sum_real_fee = dfx.groupby('sell_time')['real_fee'].sum().values[0] 
                    header_mean_real_price = dfx.groupby('sell_time')['real_price'].mean().values[0]                  
    
                    header_sum_real_qty = round(header_sum_real_qty, _q_)         # round(990.12, _q_)     
                    header_sum_real_fee = round(header_sum_real_fee, 0)           # zero(0)
                    header_mean_real_price = round(header_mean_real_price, _p_)   # round(999999.123, _p_)          
    
                    ### update sell order (real_qty, real_price, real_fee) ★
                    find_mask = sell_df['sell_time'] == sell_time
                    dfx = sell_df[find_mask]
                    # not found sell order ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL} sell order({sell_time:%Y-%m-%d %H:%M:%S}) not found: {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order => update 
                        sell_df.loc[find_mask, 'real_qty']   = header_sum_real_qty                  
                        sell_df.loc[find_mask, 'real_price'] = header_mean_real_price 
                        sell_df.loc[find_mask, 'real_fee']   = header_sum_real_fee 
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={header_sum_real_qty:,.{_q_}f}, real_price={header_mean_real_price:,.{_p_}f}, real_fee={header_sum_real_fee:,.0f}) ")                                                    
                    # end of if dfx.shape[0] == 0:          
                # end of if dfx.shape[0] == 0:  
           
                # continue to next sell order... ★
            # end of for i in range(len(sell_df)):
    
            #################################################################################
            ### 【8】SAVE BUY/SELL ORDER TO CSV FILES 
            #################################################################################        
    
            ### CLOSE (save the buy order csv file) : buy_order(BTC_JPY).csv         
            csv_file = self.folder_trading_type + f'buy_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/buy_order(BTC_JPY).csv  
            buy_df.to_csv(csv_file, header=False, index=False) # OverWrite mode  
    
            ### CLOSE (save the sell order csv file) : sell_order(BTC_JPY).csv      
            csv_file = self.folder_trading_type + f'sell_order({symbol}).csv' 
            # desktop-pc/bot//buy_sell/sell_order(BTC_JPY).csv   
            sell_df.to_csv(csv_file, header=False, index=False) # OverWrite mode 
           
            return   
    
        ################################################
        def update_partially_closed_order(self) -> None: 
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal   
            self.gvar.callers_method_name = ':: update_partially_closed_order():'
            order_completed = self.trade.get_order_status(exclude=False)   # include stop_loss, cancelled, closed order info
            
            df = self.coin.master2_df    
            mas_df = pd.DataFrame(df)   # cast df (master dataframe)
            # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}last order completed({order_completed}){Bcolors.ENDC}, mas_df.shape({mas_df.shape[0]}), close({self.gvar.close_pending_orders_request}), cancel({self.gvar.cancel_pending_orders}) ")       
    
            # mas_df is empty ? => SYTEM ERROR
            if mas_df.shape[0] == 0: 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - mas_df({mas_df.shape[0]}) is empty. skip update_order() ")
                self.debug.sound_alert(3)                  
                return 
    
            # ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            # buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            # ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            # buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)       
    
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
    
            # get_orderbooks_try() -> tuple:  # return (sell_df, buy_df)   
            sell_bk, buy_bk = self.api.get_orderbooks_try()
            # sleep(0.5)
            sell_bk_df = pd.DataFrame(sell_bk)  # ascending order of price [-1, price]      : low to high price 100, 110, 120, 130, 140, 150
            buy_bk_df = pd.DataFrame(buy_bk)    # descending order of price iloc[0, price]  : high to low price 150, 140, 130, 120, 110, 100
            
            # not empty order books ?
            if sell_bk_df.shape[0] > 0 and buy_bk_df.shape[0] > 0:                
                # get latest sell/buy price from the order books
                bk_sell_price = sell_bk_df.iloc[0]['price'] # get lowest sell price : 100
                bk_buy_price = buy_bk_df.iloc[0]['price']   # get highest buy price : 150
            else:
                bk_sell_price = 0.0
                bk_buy_price = 0.0
    
            #################################################
            ### 【1】READ ALL SELL ORDER 
            #################################################
    
            ### get all sell order 
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']   # unique time                  
                buy_time        = sell_df.loc[i, 'buy_time']    # unique time                        
                sell_qty        = sell_df.loc[i, 'qty']         
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']    
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True        
                     
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell order sub
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell order sub ?
                    if dfx.shape[0] > 0:
                        dfx.reset_index(inplace=True)                
                        sell_order_id = str(dfx.loc[0, 'sell_order_id'])    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sell_order_id})... ")                        
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_order_id}) has been closed: sell_order_closed={sell_order_closed} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_time:%Y-%m-%d %H:%M:%S}) has been closed: sell_order_closed={sell_order_closed} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # continue to next sell order     
                # end of if sell_order_closed:    
    
                ### open or incomplete sell order
    
                ##########################################################################################
                ### 【2】READ ALL SELL ORDER SUB & EXECUTE GET_PRICE() FOR EACH SELL ORDER SUB
                ##########################################################################################
    
                ### for sell order sub : use pandas dataframe groupby()            
    
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order : 2 rows
                for j in range(len(sell_subx_df)):   # ★ sell_subx_df ★
                    sell_order_sub_seq = j + 1 # 1, 2, 3,....
    
                    ### get sell order sub info
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # S9999-999999-999999 ★ (unique id) used for get_price() api
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # B9999-999999-999999 ★ (non unique id)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # (unique id)
                    sub_sell_qty        = sell_subx_df.loc[j, 'qty']                # 10
                    sub_sell_price      = sell_subx_df.loc[j, 'price']            
                    sub_sell_real_qty   = sell_subx_df.loc[j, 'real_qty']           # 10
                    sub_sell_real_price = sell_subx_df.loc[j, 'real_price']            
                    sub_sell_real_fee   = sell_subx_df.loc[j, 'real_fee']  
                    sub_sell_stop_loss  = sell_subx_df.loc[j, 'stop_loss'] 
                    sub_sell_cancelled  = sell_subx_df.loc[j, 'cancelled']        
    
                    sub_sell_order_open = False
                    sub_sell_order_closed = False
                    sub_sell_order_incomplete = False
    
                    if sub_sell_real_qty == 0:
                        sub_sell_order_open = True
                    elif sub_sell_qty == sub_sell_real_qty:
                        sub_sell_order_closed = True
                    else:
                        sub_sell_order_incomplete = True                             
    
                    # fake sell sub order ? => SKIP
                    if sub_sell_order_id.startswith('Fake'): 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id})... ") 
                        continue             
                        
                    # cancelled sell sub order ? => SKIP
                    if sub_sell_cancelled:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled ▶ sub_sell_cancelled={sub_sell_cancelled} ")  
                        continue
    
                    # stop loss sell sub order ? => SKIP
                    if sub_sell_stop_loss:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss ▶ sub_sell_stop_loss={sub_sell_stop_loss} ")  
                        continue            
    
                    # closed sell sub order ? => SKIP 
                    if sub_sell_order_closed:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed ▶ sub_sell_order_closed={sub_sell_order_closed} ")  
                        continue     
    
                    ### open or incomplete sell order sub
     
                    ##########################################################################################
                    ### 【3】GET ORDER STATUS
                    ##########################################################################################
    
                    ### get order_status
                    qty = sub_sell_qty
                    price = sub_sell_price
    
                    # get_order_status_try(order_id: str, qty: float, price: float) ->tuple:    # return (status, order_status)                  
                    status, order_status = self.api.get_order_status_try(sub_sell_order_id, qty, price)     # ★ qty, price are used for fake mode only 
                    # status: 0(ok), 4(invalid request), 8(retry error), 9(internal error), ?(misc error) 
    
                    ### status = 0; order_status = 'EXPIRED'    # DEBUG DEBUG DEBUG
    
                    # get_order_status() error ?
                    if status != 0:                       
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}) error ▶ {Bcolors.FAIL} {status=}, {order_status=} {Bcolors.ENDC} ")                               
                        continue    # continue to next sell order sub                       
    
                    ### get_order_status() : status == 0, order_status in 'WAITING,MODIFYING,CANCELLING,ORDERED,EXECUTED,EXPIRED,CANCELED'
    
                    # pending exe_sell_close_order_limit() request ? : ORDERED(指値注文有効中), EXECUTE(全量約定)
                    if order_status in 'WAITING,MODIFYING,CANCELLING,ORDERED':                               
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}get_order_status_try({sub_sell_order_id}, {qty:,.{_q_}f}, {price:,.{_p_}f}){Bcolors.ENDC} ▶ {order_status=} pending exe_sell_close_order_limit() request ")                         
                        continue    # continue to next sell order sub                  
            
                    ### executed exe_sell_close_order_limit() : order_status in 'EXECUTED(全量約定),EXPIRED(全量失効|一部失効),CANCELED'       
    
                    if order_status in 'EXPIRED,CANCELED':                      
                        ### update sell order, sell order sub and order csv file
                        if order_status == 'CANCELED':
                             self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}): {Bcolors.FAIL}the order has been cancelled by GMO. please confirm the reason{Bcolors.ENDC} ")   
                             self.debug.sound_alert(3)
                             continue    # continue to next sell order sub 
    
                        if order_status == 'EXPIRED':     
                            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}): {Bcolors.FAIL}the order has been expired due to loss cut by GMO{Bcolors.ENDC} ")   
                            self.debug.sound_alert(3)
    
                            ### close sell order sub
    
                            # update a sell order sub (update real_qty, real_price, stop_loss)                                             
                            # DO NOT USE: self.csv.update_sell_order_sub(sell_time, sub_order_id, symbol, col_name_list, col_value_list) 
                            find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                            dfx = sell_sub_df[find_mask]
                            # not found sell order sub ?
                            if dfx.shape[0] == 0:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found: {dfx.shape=}{Bcolors.ENDC} ")   
                            else: # found sell order sub => update sell order sub (real_qty, real_price, real_fee, stop_loss) 
                                sell_sub_df.loc[find_mask, 'real_qty']   = sub_sell_qty  
                                sell_sub_df.loc[find_mask, 'real_price'] = 9_999_999.0  # gmo loss cut rate 
                                sell_sub_df.loc[find_mask, 'real_fee']   = 0.0 
                                sell_sub_df.loc[find_mask, 'stop_loss'] = True
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub({sub_sell_order_id=}, real_qty={sub_sell_qty:,.{_q_}f}, real_price={9_999_999.0:,.{_p_}f}, real_fee={0.0:,.0f}, stop_loss=True) ")   
                            # end of if dfx.shape[0] == 0:  
    
                            ### close sell order      
    
                            # update sell order (real_qty, real_price, real_fee) 
                            find_mask = sell_df['sell_time'] == sell_time
                            dfx = sell_df[find_mask]
                            # not found sell order ?
                            if dfx.shape[0] == 0:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL} ({sell_time=:%Y-%m-%d %H:%M:%S}) not found: {dfx.shape=}{Bcolors.ENDC} ")   
                            else: # found sell order 
                                sell_df.loc[find_mask, 'real_qty']   = sell_qty                 
                                sell_df.loc[find_mask, 'real_price'] = 9_999_999.0    # gmo loss cut rate 
                                sell_df.loc[find_mask, 'real_fee']   = 0.0 
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={sell_qty:,.{_q_}f}, real_price={9_999_999.0:,.{_p_}f}, real_fee={0.0:,.0f}) ")                                                    
                            # end of if dfx.shape[0] == 0:                                                      
    
                            ### update order (update stop_loss)                
               
                            # update_order_csv_buy(buy_time: utcnow, buy_order_id: str, col_name_list: list, col_value_list: list) -> None: # PK(buy_time + buy_order_id)
                            self.csv.update_order_csv_buy(sub_buy_time, sub_buy_order_id, col_name_list=['stop_loss'], col_value_list=[True])                            
    
                            continue    # continue to next sell order sub                  
                        # end of if order_status == 'EXPIRED':     
                    # end of if order_status in 'EXPIRED,CANCELED':                
    
    
                    ### get highest buy price from buy orderbooks
                    latest_book_price = bk_buy_price    # bk_buy_price = buy_bk_df.iloc[0]['price']    
    
                    ##########################################################################################
                    ### 【4】GET ORDER PRICE
                    ##########################################################################################
    
                    # ### get real qty, sell price, fee from the GMO
                    # ### get the latest close price from the GMO master              
                    # ### mas_latest_close_price = mas_df.iloc[-1]['close']   # get latest close price (GMO)              
    
                    _debug1_ = self.coin.debug_mode_debug1; _debug2_ = self.coin.debug_mode_debug2    
    
                    ######################################################################################### DEBUG(1), DEBUG(2)           
                    # _debug1_ = True; _debug2_ = True    # ★ TEMP TEMP TEMP ★   
                    # self.debug.trace_warn(f".... DEBUG _debug1_ = True; _debug2_ = True ")              
                    ######################################################################################### DEBUG(1), DEBUG(2)  
    
                    ### get sell real qty, price & fee from the GMO : get_price() for exe_sell_close_order_limit() ★     
                    
                    sleep(1)    # sleep 1 sec            
                    # get_price_try(order_id: str, qty: float, price: float, latest_book_price=0.0, debug1=False, debug2=False) -> tuple: 
                    status, res_df, msg_code, msg_str = self.api.get_price_try(sub_sell_order_id, sub_sell_qty, sub_sell_price, latest_book_price, _debug1_, _debug2_)  # ★ qty, price, close_price are used for fake mode                  
                    # status: 0(ok),  8(retry error), 9(empty dataframe or internal error), ?(misc error)
                    sleep(1)    # sleep 1 sec            
    
                    res_df = pd.DataFrame(res_df)   # cast dataframe
    
                    # not real mode and debug (stop loss or cancel pending orders) ?
                    ######################################################################################### DEBUG(1_2) DEBUG(1_2) DEBUG(1_2) : update_order()
                    if not self.coin.real_mode:
                        if self.coin.debug_mode_debug1: 
                            if res_df.shape[0] > 0:
                                # ★ create two sell order subs for each sell order header ★
                                # update_get_price_response_for_debug1_2(res_df: pd.DataFrame, sell_order_sub_seq: int) -> pd.DataFrame
                                res_df = self.trade.update_get_price_response_for_debug1_2(res_df, sell_order_sub_seq)  # 1, 2, 3, ...
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_get_price_response_for_debug1_2({sell_order_sub_seq=}) ▶ {res_df.shape=} ")  
                    ######################################################################################## DEBUG(1_2) DEBUG(1_2) DEBUG(1_2) : update_order()        
    
    
                    ####################################################################################### DEBUG(2) DEBUG(2) DEBUG(2) : update_order()
                    if not self.coin.real_mode:
                        if self.coin.debug_mode_debug2:
                            # ★ create outstanding sell order sub depending on get_price_count ★
                            # get sell 'get_price_count'
                            find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                            dfx = sell_sub_df[find_mask]
                            # found sell order sub ?
                            if dfx.shape[0] > 0:
                                ix = dfx.index[0]                                    
                                get_price_count = sell_sub_df.loc[ix, 'get_price_count'] # sell_sub_df.loc[find_mask, 'get_price_count'].values[0]   # 0,1,2,3,4,5,...
                                # update_get_price_response_for_debug2(res_df: pd.DataFrame, get_price_count: int) -> pd.DataFrame:
                                res_df = self.trade.update_get_price_response_for_debug2(res_df, get_price_count)                                
                                if res_df.shape[0] == 0:
                                    status = 9                          # set internal status code
                                    msg_code = 'ERR-999'                # null => ERR-999
                                    msg_str = 'DataFrame is empty'      # null => DataFrame is empty  
                                else:
                                    status = 0
                                    msg_code = 'OK'
                                    msg_str = 'Normal Return'                                                            
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_get_price_response_for_debug2({get_price_count=}) ▶ {status=}, {res_df.shape=} ")         
                    ###################################################################################### DEBUG(2) DEBUG(2) DEBUG(2) : update_order()
                 
                    # get_price() error ?
                    if status != 0:     
                        if status == 9: # pending exe_sell_close_order_limit() request ? 
                            # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}get_price({sub_sell_price:,.{_p_}f}){Bcolors.ENDC} ▶ {status=} pending exe_sell_close_order_limit() request: continue to next sell order sub... ") 
                            status = 0  # reset status code
                            continue    # continue to next sell order sub                  
                        else: # status == 1 'ERR-5008' Timestamp for this request is too late
                            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_price({sub_sell_price:,.{_p_}f}) ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str} {Bcolors.ENDC} ")                               
                            continue    # continue to next sell order sub 
                        # end of if status == 9: 
                    else:   # normal return => executed exe_sell_close_order_limit() request                
                        pass    # self.debug.sound_alert(1) 
                    # end of if status != 0: 
    
                    ### no error : res_df.shape[0] >= 1 (single or multiple rows)  
     
                    ##############################################################################################################################
                    ### 【5】ADD A SELL ORDER CHILD (sell_time, sell_order_id, real_qty, real_price, real_fee) + (buy_time, buy_order_id)
                    ##############################################################################################################################                       
    
                    # response df for get_price(): exe_sell_close_order_limit()
                    for k in range(len(res_df)):
                        sell_order_id   = res_df.loc[k, 'orderId']      # sell order id (unique id)
                        position_id     = res_df.loc[k, 'positionId']   # position id (non unique id => same id)
                        real_qty        = res_df.loc[k, 'size']         # real qty
                        real_price      = res_df.loc[k, 'price']        # real price
                        real_fee        = res_df.loc[k, 'fee']          # real fee         
    
                        if not self.coin.real_mode:
                            position_id = sub_position_id                        
        
                        ### write a sell order child ★      
                        # write_sell_order_child(sell_time: utcnow, sell_order_id: str, buy_time: utcnow, buy_order_id: str, position_id: str, qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:               
                        _qty_ = real_qty; _price_ = real_price
                        self.csv.write_sell_order_child(sub_sell_time, sell_order_id, sub_buy_time, sub_buy_order_id, position_id, _qty_, _price_, real_qty, real_price, real_fee)    
                    # end of for k in range(len(res_df)):     
    
                    # continue to next sell order sub
                # end of for j in range(len(sell_subx_df)):
                    
                # continue to next sell order... ★
            # end of for i in range(len(sell_df)):     
    
            return   
    
    

    click image to zoom!
    図6-1
    図6-1はデモプログラムの実行結果です。 この画面は指値の売り注文が約定したときの画面です。 Apiクラスのget_order_status()の戻り値が「EXECUTED」になっているので約定しています。 Apiクラスのget_price()では約定した売り注文の数量、金額(レート)等を取得しています。


    click image to zoom!
    図6-2
    図6-2は買い・売り注文のときに作成したCSVファイルをExcelで開いた画面です。 「buy_order_(XRP_JPY).csv」ファイルには「buy_time, qty, price, real_qty, real_price,..」などが格納されています。 「real_qty, real_price」は約定した数量と金額(レート)です。

    「buy_order_sub(XRP_JPY).csv」ファイルには「buy_order_id, position_id, real_qty, real_price,...」などが格納されています。 「position_id」は売り注文を出すときに使います。

    「buy_order_child(XRP_JPY).csv」ファイルには「buy_order_id, position_id, real_qty, real_price,...」などが格納されています。 「sell_order_child(XRP_JPY).csv」ファイルには「sell_order_id, position_id, qty, price, real_qty, real_price,...」などが格納されています。 ここでは数量「40」で売り注文を出したのに数量が「20, 10, 10」に分割されて約定しています。


    click image to zoom!
    図6-3
    図6-3は売り注文で作成されたCSVファイルをExcelで開いた画像です。 「sell_order(XRP_JPY).csv」ファイルには「sell_time, qty, price, real_qty, real_price, buy_time,...」などが格納されています。 「real_qty, real_price」は約定した数量と金額(レート)です。

    「sell_order_sub(XRP_JPY).csv」ファイルには「sell_time, sell_order_id, position_id, qty, price, real_qty, real_price, buy_order_id,...」などが格納されています。

    「sell_order_child(XRP_JPY).csv」ファイルには「sell_time, sell_order_id, position_id, real_qty, real_price, buy_order_id,..」などが格納されています。 ここでは数量「40」で売り注文を出したのに数量が「20, 10, 10」に分割されて約定しています。


    click image to zoom!
    図6-4
    図6-4は仮想通貨(BTC_JPY)のレバレッジ取引で作成されたCSVファイルです。 このデモプログラムでは買い注文を数量「0.04BTC_JPY)で出しています。 そして売りでは数量「0.04BTC_JPY」で注文していますが数量が「0.02, 0.01, 0.01」に分割されて執行されています。

    「order(BTC_JPY).xlsx」ファイルにはこの取引の損益が作成されています。 「63」円の損失になっていますが、デモプログラムは強制的に約定させているので金額は無視してください。 BOTでは損益を仮想通貨ごとに集計してリアルタイムで画面に表示します。


    click image to zoom!
    図6-5
    図6-5は仮想通貨「ETH_JPY」の実行結果です。 買いでは数量「0.4ETH_JPY」で注文しています。 売りでも同じ数量「0.4ETH_JPY」で注文していますが、数量が「0.2, 0.1, 0.1」に分割されて約定されています。

    損益ファイルでは「54」円の損失になっていますが、デモプログラムは強制的に約定させているので金額は無視してください。


    click image to zoom!
    図6-6
    図6-6は仮想通貨「LTC_JPY」の実行結果です。 デモプログラムは成行きの買いを数量「4LTC_JPY」で出していますが、 FAK(Fill and Kill)が適用されて「2LTC_JPY」だけ執行されています。 執行された「2LTC_JPY」は、さらに「1LTC_JPY」と「1LTC_JPY」に分割されています。

    デモプログラムは残数「2LTC_JPY」に対して再度成行きの買い注文を出して約定しています。 結果的に2個の買い注文IDが生成されたことになります。

    売りの指値の注文ではポジションID指定で3回の売り注文(決済)を出しています。 売り注文の数量は「1, 1, 2」になります。 ところがこの売り注文は分割されて結果的に執行された数量は「1, 1, 1, 1」になっています。 売りの注文IDは3個生成されています。

    損益ファイルには2個の注文に分割されて作成されています。 2個の注文とも「20」円の損失になっていますが、デモプログラムは強制的に約定させているので金額は無視してください。


  7. BuySellクラスにupdate_order_profit()メソッドを追加する

    ここではBuySellクラスにupdate_order_profit()メソッドを追加します。 このメソッドは取引の損益を計算してCSVファイルとExcelファイルに保存します。

    buy_sell.py:
        ####################################################### 
        def update_order_profit(self, first_pass=True) -> None:
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal
    
            self.gvar.callers_method_name = 'update_order_profit():'
            order_completed = self.trade.get_order_status(exclude=False)   # include stop_loss, cancelled, closed order info
            self.debug.trace_write(f".... {self.gvar.callers_method_name} first_pass({first_pass}), last_order_completed({order_completed}) ")   
    
            ######################################################################################################################################################################
            ### 【1】IMPORT BUY ORDER SUB & APPEND/UPDATE ORDER CSV FILE (buy_time, buy_order_id, buy_real_qty, buy_real_price, buy_real_fee) & (sell_time, sell_order_id)
            ######################################################################################################################################################################
    
            ### OPEN (load the order csv file) : order(BTC_JPY).csv 
            ord_df = self.csv.get_order_csv()               
            
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv
            buy_sub_df = self.csv.get_buy_order_sub()         
    
            ### import buy order sub file       
    
            # get all buy order sub
            for i in range(len(buy_sub_df)):
                ### get buy order sub info
                sub_buy_time            = buy_sub_df.loc[i, 'buy_time']             # ★ (non unique)
                sub_sell_time           = buy_sub_df.loc[i, 'sell_time']            # ★ (non unique)
                sub_buy_order_id        = buy_sub_df.loc[i, 'buy_order_id']         # ★ 'B9999-999999-999999' (unique)
                sub_sell_order_id       = buy_sub_df.loc[i, 'sell_order_id']        # ★ 'S9999-999999-999999;S9999-999999-999999;S9999-999999-999999' (list of sell_order_ids) : buy sub(one) <= sell sub(many)
                sub_buy_qty             = buy_sub_df.loc[i, 'qty']
                sub_buy_price           = buy_sub_df.loc[i, 'price']
                sub_buy_real_qty        = buy_sub_df.loc[i, 'real_qty']                
                sub_buy_real_price      = buy_sub_df.loc[i, 'real_price']
                sub_buy_real_fee        = buy_sub_df.loc[i, 'real_fee']
                sub_buy_get_price_count = buy_sub_df.loc[i, 'get_price_count']    
                sub_buy_stop_loss       = buy_sub_df.loc[i, 'stop_loss']
                sub_buy_order_cancelled = buy_sub_df.loc[i, 'cancelled']
                sub_buy_order_closed = True if sub_buy_qty == sub_buy_real_qty else False                  
           
                # fake sub order ? => SKIP
                if sub_buy_order_id.startswith('Fake'): 
                    # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake buy order sub({sub_buy_order_id})... ") 
                    continue                              
    
                # empty order csv file ?
                if ord_df.shape[0] == 0:
                    dfx = pd.DataFrame()
                else:   # not empty dataframe => find the order csv file by buy_order_id
                    find_mask = ord_df['buy_order_id'] == sub_buy_order_id  # B9999-999999-999999
                    dfx = ord_df[find_mask]
                
                # not found order csv file ?
                if dfx.shape[0] == 0:                 
                    ### append order csv file ★IMPORTANT★ DO NOT APEEND 'Null' Value => sell_order_id = 'S9999-999999-999999'
                    columns = Gmo_dataframe.ORDER_CSV
                    _sell_real_qty_=0.0; _sell_real_price_=0.0; _sell_real_fee_=0.0; _real_profit_=0.0
                    _accumulated_leverage_fee_=0.0; _close_count_=0; _close_update_date_=dt.datetime.now()
                    _order_closed_ = False; _stop_loss_=sub_buy_stop_loss; _cancelled_=sub_buy_order_cancelled
                    _order_open_date_ = dt.datetime.now(); _order_close_date_ = dt.datetime.now(); _log_time_=dt.datetime.now()
                    data = [[sub_buy_time, sub_sell_time, sub_buy_order_id, sub_sell_order_id, sub_buy_real_qty, _sell_real_qty_, sub_buy_real_price, _sell_real_price_, sub_buy_real_fee, _sell_real_fee_, _real_profit_, _accumulated_leverage_fee_, _close_count_, _close_update_date_, _order_closed_, _stop_loss_, _cancelled_, _order_open_date_, _order_close_date_, _log_time_]] 
                    new_df = pd.DataFrame(data, columns=columns)                                           
    
                    # convert datatypes (buy_time, sell_time, log_time, buy_order_id, sell_order_id)
                    new_df['buy_time']  = pd.to_datetime(new_df['buy_time'], utc=True)          # object => utc time (aware)
                    new_df['sell_time'] = pd.to_datetime(new_df['sell_time'], utc=True)         # object => utc time (aware)   
                    new_df['close_update_date']  = pd.to_datetime(new_df['close_update_date'])  # object => ns time (jp time)   
                    new_df['order_open_date']    = pd.to_datetime(new_df['order_open_date'])    # object => ns time (jp time) 
                    new_df['order_close_date']   = pd.to_datetime(new_df['order_close_date'])   # object => ns time (jp time)                    
                    new_df['log_time']  = pd.to_datetime(new_df['log_time'])                    # object => ns time (jp time)    
    
                    new_df['buy_order_id']  = new_df['buy_order_id'].astype(str)                # int64 => string
                    new_df['sell_order_id'] = new_df['sell_order_id'].astype(str)               # int64 => string 
    
                    # append a new row (order row)
                    ord_df = ord_df.append(new_df, ignore_index=True)                                      
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} write_order_csv_buy(buy_order_id={sub_buy_order_id}, buy_real_qty={sub_buy_real_qty:,.{_q_}f}, buy_real_price={sub_buy_real_price:,.{_p_}f}, buy_real_fee={sub_buy_real_fee:,.0f}) ")                              
                else: # found order csv file => update order csv file
                    # get buy_real_qty and sell_real_qty
                    ix = dfx.index[0]   # get index of the first row
                    # ord_buy_order_id = ord_df.loc[ix, 'buy_order_id']
                    ord_buy_real_qty = ord_df.loc[ix, 'buy_real_qty']
                    ord_sell_real_qty = ord_df.loc[ix, 'sell_real_qty']               
                    order_closed = ord_df.loc[ix, 'order_closed']   
                                  
                    # completed order csv file ?
                    if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and ord_buy_real_qty == ord_sell_real_qty:
                        if not order_closed:
                            # update order csv file 
                            ord_df.loc[find_mask, 'order_closed']   = True      
                            ord_df.loc[find_mask, 'order_close_date'] = dt.datetime.now() 
                            ord_df.loc[find_mask, 'log_time'] = dt.datetime.now()    
    
                            ord_df['order_close_date']   = pd.to_datetime(ord_df['order_close_date'])  # object => ns time (jp time)                         
                            ord_df['log_time']  = pd.to_datetime(ord_df['log_time'])    # object => ns time (jp time)                          
                        # self.debug.trace_write(f".... update_order2(): pass update order info (buy): buy_order_id={sub_buy_order_id}, buy_real_qty={sub_buy_real_qty:,.{_q_}f}, buy_real_price={sub_buy_real_price:,.{_x_}f}, buy_real_fee={sub_buy_real_fee:,.0f}  ")                              
                    else: # incomplete order => update order csv file                                        
                        # update order info 
                        ord_df.loc[find_mask, 'buy_time']       = sub_buy_time      
                        ord_df.loc[find_mask, 'buy_order_id']   = sub_buy_order_id
                        ord_df.loc[find_mask, 'buy_real_qty']   = sub_buy_real_qty                
                        ord_df.loc[find_mask, 'buy_real_price'] = sub_buy_real_price
                        ord_df.loc[find_mask, 'buy_real_fee']   = sub_buy_real_fee 
                        ord_df.loc[find_mask, 'log_time'] = dt.datetime.now()    
    
                        ### update order_closed flag
    
                        # get buy_real_qty and sell_real_qty
                        ix = dfx.index[0]   # get index of the first row
                        # ord_buy_order_id = ord_df.loc[ix, 'buy_order_id']
                        ord_buy_real_qty = ord_df.loc[ix, 'buy_real_qty']
                        ord_sell_real_qty = ord_df.loc[ix, 'sell_real_qty']               
                        order_closed = ord_df.loc[ix, 'order_closed']          
                        if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and ord_buy_real_qty == ord_sell_real_qty:  
                            ord_df.loc[find_mask, 'order_closed'] = True           
    
                        # convert datatypes (buy_time, sell_time, log_time, buy_order_id, sell_order_id)
                        ord_df['buy_time']  = pd.to_datetime(ord_df['buy_time'], utc=True)      # object => utc time (aware)
                        ord_df['sell_time'] = pd.to_datetime(ord_df['sell_time'], utc=True)     # object => utc time (aware)      
                        ord_df['close_update_date']  = pd.to_datetime(ord_df['close_update_date']) # object => ns time (jp time)   
                        ord_df['order_open_date']    = pd.to_datetime(ord_df['order_open_date'])   # object => ns time (jp time) 
                        ord_df['order_close_date']   = pd.to_datetime(ord_df['order_close_date'])  # object => ns time (jp time)                      
                        ord_df['log_time']  = pd.to_datetime(ord_df['log_time'])                # object => ns time (jp time)    
    
                        ord_df['buy_order_id']  = ord_df['buy_order_id'].astype(str)            # int64 => string
                        ord_df['sell_order_id'] = ord_df['sell_order_id'].astype(str)           # int64 => string                   
        
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_order_csv_buy(buy_order_id={sub_buy_order_id}, buy_real_qty={sub_buy_real_qty:,.{_q_}f}, buy_real_price={sub_buy_real_price:,.{_p_}f}, buy_real_fee={sub_buy_real_fee:,.0f}) ")                              
                    # end of if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and ord_buy_real_qty == ord_sell_real_qty:                             
                # end of if dfx.shape[0] == 0:
                # continue to next buy sub orders
            # end of for i in range(len(buy_sub_df)):	           
    		
            #######################################################################################################################################
            ### 【2】IMPORT SELL ORDER SUB & UPDATE ORDER CSV FILE (sell_time, sell_order_id, sell_real_qty, sell_real_price, sell_real_fee)
            #######################################################################################################################################
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv
            sell_sub_df = self.csv.get_sell_order_sub()	
    
            ### import sell order sub file      
    
            for i in range(len(sell_sub_df)):
                ### get sell order sub info
                sub_sell_time               = sell_sub_df.loc[i, 'sell_time']           # ★ (non unique)
                sub_buy_time                = sell_sub_df.loc[i, 'buy_time']            # ★ (non unique)
                sub_sell_order_id           = sell_sub_df.loc[i, 'sell_order_id']       # ★ 'S9999-999999-999999' (unique id)
                sub_buy_order_id            = sell_sub_df.loc[i, 'buy_order_id']        # ★ 'B9999-999999-999999' (non unique id) : sell sub(many) => buy_sub(one)
                sub_sell_qty                = sell_sub_df.loc[i, 'qty']
                sub_sell_price              = sell_sub_df.loc[i, 'price']
                sub_sell_real_qty           = sell_sub_df.loc[i, 'real_qty']
                sub_sell_real_price         = sell_sub_df.loc[i, 'real_price']
                sub_sell_real_fee           = sell_sub_df.loc[i, 'real_fee']
                sub_sell_get_price_count    = sell_sub_df.loc[i, 'get_price_count']     
                sub_sell_stop_loss          = sell_sub_df.loc[i, 'stop_loss']         
                sub_sell_order_cancelled    = sell_sub_df.loc[i, 'cancelled']
    
                sub_sell_order_open = False
                sub_sell_order_closed = False
                sub_sell_order_incomplete = False
    
                if sub_sell_real_qty == 0:
                    sub_sell_order_open = True
                elif sub_sell_qty == sub_sell_real_qty:
                    sub_sell_order_closed = True
                else:
                    sub_sell_order_incomplete = True       
    
                # fake sub order ? => SKIP
                if sub_sell_order_id.startswith('Fake'): 
                    # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id})... ") 
                    continue             
                                           
                # empty order csv file ?           
                if ord_df.shape[0] == 0:
                    self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - order csv file is empty: continue... ")
                    self.debug.sound_alert(3)  
                    continue
    
                # find the order csv file by buy_order_id        
                find_mask = ord_df['buy_order_id'] == sub_buy_order_id
                dfx = ord_df[find_mask]
                # not found order csv file ?
                if dfx.shape[0] == 0:
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}ERROR{Bcolors.ENDC} order csv file not found: buy_order_id={sub_buy_order_id} ")  
                    self.debug.sound_alert(3)  
                    continue                
    
                ### found order csv file
                
                # get buy_real_qty and sell_real_qty
                ix = dfx.index[0]   # get index of the first row
                ord_sell_order_id = ord_df.loc[ix, 'sell_order_id']
                ord_buy_real_qty  = ord_df.loc[ix, 'buy_real_qty']
                ord_sell_real_qty = ord_df.loc[ix, 'sell_real_qty']            
                ord_order_closed = ord_df.loc[ix, 'order_closed']   
                        
                # completed order csv file ?
                if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and ord_buy_real_qty == ord_sell_real_qty: 
                    if not ord_order_closed:
                        # update order csv file 
                        ord_df.loc[find_mask, 'order_closed']   = True      
                        ord_df.loc[find_mask, 'order_close_date'] = dt.datetime.now() 
                        ord_df.loc[find_mask, 'log_time'] = dt.datetime.now()    
    
                        ord_df['order_close_date']   = pd.to_datetime(ord_df['order_close_date'])  # object => ns time (jp time)                         
                        ord_df['log_time']  = pd.to_datetime(ord_df['log_time'])    # object => ns time (jp time)             
                    # self.debug.trace_write(f".... update_order2(): pass update order csv file (sell): sell_order_id={sub_sell_order_id}, sell_real_qty={sub_sell_real_qty:,.{_q_}f}, sell_real_price={sub_sell_real_price:,.{_x_}f}, sell_real_fee={sub_sell_real_fee:,.0f}  ")                              
                else: # incomplete order => update order csv file
    
                    ### get a list of sell_order_id from the sell order sub ★ buy_order_id ★
                    filter_mask = sell_sub_df['buy_order_id'] == sub_buy_order_id
                    dfx = sell_sub_df[filter_mask]    
                    sell_order_id_list = dfx.groupby('buy_order_id')['sell_order_id'].apply(list)[0]     
                    sell_order_ids_by_semicolon = ''
                    for k, order_id in enumerate(sell_order_id_list):
                        if k == 0:
                            sell_order_ids_by_semicolon += order_id 
                        else:
                            sell_order_ids_by_semicolon += ';' + order_id 
    
                    ### calculate total sell qty / fee and mean price ★ buy_order_id ★
                    sell_sum_real_qty = dfx.groupby('buy_order_id')['real_qty'].sum().values[0]    
                    sell_sum_real_fee = dfx.groupby('buy_order_id')['real_fee'].sum().values[0] 
                    sell_mean_real_price = dfx.groupby('buy_order_id')['real_price'].mean().values[0]                           
    
                    ### update order csv file sell info (sell_time, sell_order_id, sell_real_qty, sell_real_price, sell_real_fee) OK
                    ord_df.loc[find_mask, 'sell_time']       = sub_sell_time    
                    ord_df.loc[find_mask, 'sell_order_id']   = sell_order_ids_by_semicolon      # 'OrderId1;OrderId2;OrderId3'                       
                    ord_df.loc[find_mask, 'sell_real_qty']   = round(sell_sum_real_qty, 2)      # 999.12         
                    ord_df.loc[find_mask, 'sell_real_price'] = round(sell_mean_real_price, 3)   # 999999.123  
                    ord_df.loc[find_mask, 'sell_real_fee']   = round(sell_sum_real_fee, 3)      # 999.123     
    
                    # convert datatypes (buy_time, sell_time, log_time, buy_order_id, sell_order_id)
                    ord_df['buy_time']  = pd.to_datetime(ord_df['buy_time'], utc=True)          # object => utc time (aware)
                    ord_df['sell_time'] = pd.to_datetime(ord_df['sell_time'], utc=True)         # object => utc time (aware)   
                    ord_df['close_update_date']  = pd.to_datetime(ord_df['close_update_date'])  # object => ns time (jp time)     
                    ord_df['order_open_date']    = pd.to_datetime(ord_df['order_open_date'])    # object => ns time (jp time) 
                    ord_df['order_close_date']   = pd.to_datetime(ord_df['order_close_date'])   # object => ns time (jp time)                 
                    ord_df['log_time']  = pd.to_datetime(ord_df['log_time'])                    # object => ns time (jp time)    
    
                    ord_df['buy_order_id']  = ord_df['buy_order_id'].astype(str)                # int64 => string
                    ord_df['sell_order_id'] = ord_df['sell_order_id'].astype(str)               # int64 => string               
    
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} update_order_csv_sell(sell_order_id={sub_sell_order_id}, sell_real_qty={sell_sum_real_qty:,.{_q_}f}, sell_real_price={sell_mean_real_price:,.{_p_}f}, sell_real_fee={sell_sum_real_fee:,.0f}) ")                              
    
                # end of if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and ord_buy_real_qty == ord_sell_real_qty:    
                # continue to next sell order sub
            # end of for i in range(len(sell_sub_df)):
    
            #######################################################################################################################################
            ### 【3】SAVE ORDER TO CSV/EXCEL FILES
            #######################################################################################################################################
    
            ### CLOSE (save to the order csv file) : order(BTC_JPY).csv
            if ord_df.shape[0] > 0:
                ### CLOSE (order csv file)
    
                csv_file = self.folder_trading_type + f'order({symbol}).csv'  
                # desktop-pc/bot//buy_sell/order(BTC_JPY).csv   
                ord_df.to_csv(csv_file, header=False, index=False) # overWrite the order file  
         
                ### SAVE (order excel file)
    
                # save to the order excel file
                excel_file = self.folder_trading_type + f'order({symbol}).xlsx'
                # desktop-pc/bot/buy_sell/order(BTC_JPY).xlsx   
    
                excel_df = ord_df.copy()
    
                # (1) convert time to string              
                excel_df['buy_time']  = excel_df['buy_time'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))  
                excel_df['sell_time'] = excel_df['sell_time'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))   
                excel_df['close_update_date']  = excel_df['close_update_date'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))         
                excel_df['log_time']  = excel_df['log_time'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))  
    
                # (2) drop real_profit column
                excel_df = excel_df.drop(['real_profit'], axis=1)            
    
                # (3) add new columns
                excel_df['buy(SUM)']    = '=ROUND(E2*G2,3)'                     # add a new column
                excel_df['sell(SUM)']   = '=ROUND(F2*H2,3)'                     # add a new column
                excel_df['profit(AMT)'] = '=IF((H2-G2)>0,ROUNDDOWN((H2 - G2) * F2, 0),ROUNDUP((H2 - G2) * F2, 0))'  # add a new column : rounddown(profit), roundup(loss)
                excel_df['profit(%)']   = '=M2/K2'                              # add a new column
    
                # (4) update excel formula
    
                # get all excel order info  
                row = 2
                for i in range(len(excel_df)):
                    order_closed = excel_df.loc[i, 'order_closed']              # True or False
                    stop_loss = excel_df.loc[i, 'stop_loss']                    # True or False
                    cancelled = excel_df.loc[i, 'cancelled']                    # True or False
                    if order_closed or stop_loss or cancelled:
                        cell = str(row)
                        excel_df.loc[i, 'buy(SUM)']     = f'=ROUND(E{cell}*G{cell},3)'                     
                        excel_df.loc[i, 'sell(SUM)']    = f'=ROUND(F{cell}*H{cell},3)'                     
                        excel_df.loc[i, 'profit(AMT)']  = f'=IF((H{cell}-G{cell})>0,ROUNDDOWN((H{cell} - G{cell}) * F{cell}, 0),ROUNDUP((H{cell} - G{cell}) * F{cell}, 0))'  
                        excel_df.loc[i, 'profit(%)']    = f'=M{cell}/K{cell}'                   
                    else:
                        excel_df.loc[i, 'buy(SUM)']     = ''                     
                        excel_df.loc[i, 'sell(SUM)']    = ''                     
                        excel_df.loc[i, 'profit(AMT)']  = ''
                        excel_df.loc[i, 'profit(%)']    = ''                       
                    row += 1
                # end of for j in range(len(excel_df)):    
    
                # (5) re-order columns
                columns = [
                    'buy_time','sell_time','buy_order_id','sell_order_id','buy_real_qty','sell_real_qty','buy_real_price','sell_real_price','buy_real_fee','sell_real_fee',
                    'buy(SUM)','sell(SUM)','profit(AMT)','profit(%)',
                    'accumulated_leverage_fee','close_count','close_update_date',
                    'order_closed','stop_loss','cancelled','order_open_date','order_close_date','log_time'
                    ]    
                       
                excel_df = excel_df[columns]
            
                # print('excel_df.info()\n', excel_df.info())
    
                # ------------------------------------------------------------------------------------------
                #  #   Column                Dtype
                # ------------------------------------------------------------------------------------------
                #  1   time(B)               datetime64[utc]    => Convert to string
                #  2   time(S)               datetime64[utc]    => Convert to string
                #  3   order(B)              object  
                #  4   order(S)              object    
                #  5   qty(B)                float64    
                #  6   qty(S)                float64                
                #  7   price(B)              float64
                #  8   price(S)              float64
                #  9   fee(B)                float64
                # 10   fee(S)                float64
                #      real_profit           float64             => REMOVE
                # 11   buy(SUM)              float64             => ADD =ROUNDUP(E2*G2,3)+I2
                # 12   sell(SUM)             float64             => ADD =ROUNDDOWN(F2*H2,3)+J2
                # 13   profit(AMT)           float64             => ADD =L2-K2             
                # 14   profit(%)             float64             => ADD =M2/K2  
                # 15   fee(L)                float64                       
                # 16   count(L)              int                 
                # 17   close(L)              datetime64[ns]            
                # 18   order_closed          bool                 
                # 19   stop                  bool
                # 20   cancel                bool
                # 21   open_date             datetime64[ns]      
                # 22   close_date            datetime64[ns]      
                # 23   log_time              datetime64[ns]      => Convert to string
                # ----------------------------------------------------------------------------------------                              
    
                # (5) rename column names & export to excel file order(BTC_JPY).xlsx
                excel_df.columns = Gmo_dataframe.ORDER_XLSX
                excel_df.to_excel(excel_file, sheet_name='GMO Orders', index=False) 
            # end of if ord_df.shape[0] > 0:
    
            return       
    

    click image to zoom!
    図7-1
    図7-1はデモプログラムの実行結果です。 このデモプログラムでは仮想通貨「XRP_JPY, LTC_JPY, BCH_JPY」に対して10回自動トレードを繰り返しています。 注文数は「10XRP_JPY, 1LTC_JPY, 0.1BCH_JPY」で利益率は「0.0035, 0.0015, 0.0015」を設定しています。 注文数は最小値で利益率も小さいので金額は無視して約定の回数に注目してください。

    画像にはBuySellクラスのupdate_order_profit()メソッドが実行されたことがログに表示されています。 このメソッドでは仮想通貨ごとの損益を計算してExcelのファイルに保存します。


    click image to zoom!
    図7-2
    図7-2はupdate_order_profit()メソッドが作成した損益ファイルをExcelで開いています。 このファイルには「time(B/S), order(B/S), qty(B/S), price(B/S), profit(AMT), profit(%), close, stop, cancel,..」などが格納されています。 「()」のBはBuy, SはSellを意味します。 「profit(AMT)」には損益額、「profit(%)」には損益をパーセント(%)で表示します。

    「closed」が「TRUE」のときは取引が約定していることを意味します。 「stop, cancel」が「TRUE」のときは取引がストップ・ロス、キャンセルされたことを意味します。 「BCH_JPY」が3回、「LTC_JPY」が1回約定しています。 BOTのログには仮想通貨ごとの約定回数と損益が集計されてリアルタイムで表示されます。


  8. BuySellクラスにstop_loss(), stop_loss_0()メソッドを追加する

    ここではBuySellクラスにstop_loss()メソッドを追加します。 このメソッドは価格が下落したときに強制的にポジションを決済して損失が大きくなるのを回避します。 stop_loss()もBOTの肝となる部分です。 buy_sell()とstop_loss()の作り方によって利益がでるかどうかが決まります。 stop_loss()の引数には「method_id」を指定します。 デフォルトで「method_id=0」が用意されています。 stop_loss_0()メソッドはBOTをテストするときに使用します。 BOTのテストが完了したら本番用のストップ・ロスのメソッドを作って 「stop_loss_1(), stop_loss_2(),...」のようなメソッド名を追加します。

    buy_sell.py:
        ############################################ 
        def stop_loss(self, method_id: int) -> None:
            """
            
            """
            
            if method_id <= 1:
                eval(f'self.stop_loss_{str(method_id)}()')    # call 'self.stop_loss_?() method
            else:
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}Invalid stoploss_method_id {method_id=} {Bcolors.ENDC} -  skip stop_loss() ")
                self.debug.sound_alert(3)           
            
            # if method_id == 0:
            #     self.stop_loss_0()  # stop loss method 0
            # elif method_id == 1:
            #     self.stop_loss_1()  # stop loss method 1
            # else:   #
            #     self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}Invalid stoploss_method_id {method_id=} {Bcolors.ENDC} -  skip stop_loss() ")
            #     self.debug.sound_alert(3)    
    
            return        
        
        ############################## 
        def stop_loss_0(self) -> None:
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal
            self.gvar.callers_method_name = 'stop_loss(0):'        
            df = self.coin.master2_df
            mas_df = pd.DataFrame(df)   # cast df (master dataframe)
            
            self.debug.trace_write(f".... {self.gvar.callers_method_name} mas_df.shape({mas_df.shape[0]}), loss cut: 18%({self.coin.buy_sell_action14}), 15%({self.coin.buy_sell_action13}), 10%({self.coin.buy_sell_action12}) ")                               
    
            # ### OPEN (load the order csv file) : order(BTC_JPY).csv 
            # ord_df = self.csv.get_order_csv()          
          
            ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)  
           
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
         
            #################################################
            ### 【1】READ ALL SELL ORDERs 
            #################################################    
    
            # define sell order list
            header_real_qty_list = []
            header_real_price_list = []
            header_real_fee_list = []        
    
            ### get all sell orders   
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']             # ★ unique time 
                buy_time        = sell_df.loc[i, 'buy_time']              # ★ unique time   
                sell_qty        = sell_df.loc[i, 'qty']
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True  
    
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell order sub
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell order sub ?
                    if dfx.shape[0] > 0:
                        ix = dfx.index[0]               
                        sell_order_id = str(dfx.loc[ix, 'sell_order_id'])    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake order sub({sell_order_id})... ")
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sell_order_id}) has been closed: {sell_order_closed=} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{BBcolors.ENDC} sell order({sell_time:%Y-%m-%d %H:%M:%S}) has been closed: ={sell_order_closed=} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # skip closed order                                                            
                # end of if sell_order_closed:
                    
                #################################################################
                ### 【2】READ SELL ORDER SUB (open or incomplete status)
                #################################################################
                         
                ### for sell order sub (open or incomplete status)
    
                # define sell order sub list
                sub_real_qty_list = []
                sub_real_price_list = []
                sub_real_fee_list = []         
    
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order 
                for j in range(len(sell_subx_df)):  # ★ sell_subx_df ★   
                    ### get sell order sub info
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # S9999-999999-999999 ★ (unique id)     
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # B9999-999999-999999 ★ (non unique id) : sell sub(many) => buy_sub(one)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # ★ (unique id)
                    sub_qty             = sell_subx_df.loc[j, 'qty']                # 40  
                    sub_price           = sell_subx_df.loc[j, 'price']            
                    sub_real_qty        = sell_subx_df.loc[j, 'real_qty']           # 20         
                    sub_real_price      = sell_subx_df.loc[j, 'real_price']            
                    sub_real_fee        = sell_subx_df.loc[j, 'real_fee']         
                    sub_get_price_count = sell_subx_df.loc[j, 'get_price_count']   
                    sub_stop_loss       = sell_subx_df.loc[j, 'stop_loss']
                    sub_cancelled       = sell_subx_df.loc[j, 'cancelled']  
    
                    sub_order_open = False
                    sub_order_closed = False
                    sub_order_incomplete = False
    
                    if sub_real_qty == 0:
                        sub_order_open = True
                    elif sub_qty == sub_real_qty:
                        sub_order_closed = True
                    else:
                        sub_order_incomplete = True                             
    
                    # fake sell sub order ? => SKIP
                    if sub_sell_order_id.startswith('Fake') :
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id}) ")  
                        continue   
    
                    # sell sub order has been stop loss ? => SKIP
                    if sub_stop_loss: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss: {sub_stop_loss=} ")  
                        continue
    
                    # sell sub order has been cancelled ? => SKIP
                    if sub_cancelled: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled: {sub_cancelled=} ")  
                        continue
    
                    # closed sell sub order ? => SKIP
                    if sub_order_closed:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed: {sub_order_closed=} ")  
                        continue  
    
                    #######################################################################################################
                    ### 【3】CHECK STOP LOSS CONDITIONS FOR EACH SELL ORDER SUB (open or incomplte sell order sub)
                    #######################################################################################################
    
                    ### open or incomplete sell order sub             
                     
                    stop_loss_detected = False
    
                    while True:
    
                        #####################################################
                        ### Check stop loss debug mode
                        #####################################################
                        if self.coin.debug_mode_stop_loss:
                            stop_loss_detected = True
                            break # exit while true loop 
    
                        #####################################################
                        ### Unconditional stop loss
                        #####################################################
    
                        ### check loss cut price : buy => sell 
    
                        ### calculate stop loss amount
    
                        stop_loss_amt = 0
                        latest_sell_price = 0
    
                        filter_mask = mas_df['side'] == 'SELL'
                        dfx = mas_df[filter_mask]        
                        if dfx.shape[0] > 0:          
                            # get latest sell price from GMO master dataframe
                            sell_price = dfx.iloc[-1]['price']
                            latest_sell_price = sell_price
    
                            # get last buy qty & price from the buy order header
                            qty = buy_df.iloc[-1]['qty']
                            buy_price = buy_df.iloc[-1]['real_price']   # get a real buy price
    
                            # calculate a stop loss amount : stop_loss_amt = math.floor((sell_price - buy_price) * qty)
                            stop_loss_amt = math.floor((sell_price - buy_price) * qty)  # positive => profit, negative => loss
                            stop_loss_amt = math.floor(stop_loss_amt) # positive => profit, negative => loss                    
    
                        ### calculate loss cut price from the buy sub order : loss_cut_price = last_buy_real_price - (last_buy_real_price * 0.20)                 
                        last_buy_real_price = buy_sub_df.iloc[-1]['real_price']      
    
                        ### buy_sell : sell position => calculate down limit for 5%, 10%, 15%, 18%, 20%, 23%, 25%(GMO loss cut rate)
    
                        # calculate loss cut price from the sell order sub  : loss_cut_price = last_buy_real_price - (last_buy_real_price * 0.20) 
    
                        loss_cut_price_05 = last_buy_real_price - (last_buy_real_price * 0.05) # 0.25(GMO loss cut rate) => 0.05 
                        loss_cut_price_10 = last_buy_real_price - (last_buy_real_price * 0.10) # 0.25(GMO loss cut rate) => 0.10             
                        loss_cut_price_15 = last_buy_real_price - (last_buy_real_price * 0.15) # 0.25(GMO loss cut rate) => 0.15 
                        loss_cut_price_18 = last_buy_real_price - (last_buy_real_price * 0.18) # 0.25(GMO loss cut rate) => 0.18 
                        loss_cut_price_20 = last_buy_real_price - (last_buy_real_price * 0.20) # 0.25(GMO loss cut rate) => 0.20 BOT LOSS CUT
                        loss_cut_price_23 = last_buy_real_price - (last_buy_real_price * 0.23) # 0.25(GMO loss cut rate) => 0.23 DO NOT USE THIS  
                        loss_cut_price_25 = last_buy_real_price - (last_buy_real_price * 0.25) # 0.25(GMO loss cut rate) => 0.25 GMO LOSS CUT
          
                        # gmo master latest sell price is greater than zero ?
                        if latest_sell_price > 0:   
    
                            if latest_sell_price <= loss_cut_price_25:      # <= 75
                                self.coin.buy_sell_lc25 += 1    
                                # self.debug.trace_warn(f".... DEBUG: loss cut 25%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_25%({loss_cut_price_25:,.{_x_}f}) ")                      
                            elif latest_sell_price <= loss_cut_price_23:    # <= 79
                                self.coin.buy_sell_lc23 += 1 
                                # self.debug.trace_warn(f".... DEBUG: loss cut 23%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_23%({loss_cut_price_23:,.{_x_}f}) ") 
                            elif latest_sell_price <= loss_cut_price_20:    # <= 80
                                self.coin.buy_sell_lc20 += 1  
                                # self.debug.trace_warn(f".... DEBUG: loss cut 20%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_20%({loss_cut_price_20:,.{_x_}f}) ")     
                            elif latest_sell_price <= loss_cut_price_18:    # <= 85
                                self.coin.buy_sell_lc18 += 1  
                                # self.debug.trace_warn(f".... DEBUG: loss cut 18%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_18%({loss_cut_price_18:,.{_x_}f}) ")                              
                            elif latest_sell_price <= loss_cut_price_15:    # <= 90
                                self.coin.buy_sell_lc15 += 1  
                                # self.debug.trace_warn(f".... DEBUG: loss cut 15%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_15%({loss_cut_price_15:,.{_x_}f}) ")                             
                            elif latest_sell_price <= loss_cut_price_10:    # <= 95
                                self.coin.buy_sell_lc10 += 1   
                                # self.debug.trace_warn(f".... DEBUG: loss cut 10%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_10%({loss_cut_price_10:,.{_x_}f}) ")                             
                            elif latest_sell_price <= loss_cut_price_05:    # <= 99
                                self.coin.buy_sell_lc5 += 1 
                                # self.debug.trace_warn(f".... DEBUG: loss cut 5%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_5%({loss_cut_price_05:,.{_x_}f}) ")                                     
    
                            ### check down limit based on buy price
    
                            # GMO latest sell price is less than or equal to loss cut price (20%) ? => EXECUTE STOP LOSS 
                            if latest_sell_price <= loss_cut_price_20:
                                self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}STOP LOSS DETECTED REAL(1-5) EXECUTE{Bcolors.ENDC} loss cut 20%: latest_sell_price({latest_sell_price:,.{_p_}f}) <= loss_cut_20({loss_cut_price_20:,.{_p_}f})  ")                   
                                stop_loss_detected = True          
                                break # exit while true loop                           
                            
                        # end of if latest_sell_price > 0:            
                                   
                        break # exit while true loop 
                    # end of while True:     
    
                    if not stop_loss_detected: continue
    
                    #######################################################
                    # ★ STOP LOSS DETECTED ★ #                                        
                    #######################################################                     
                    
                    # increment stop loss count
                    self.gvar.stop_loss_count += 1
                    self.coin.stop_loss_count += 1  
    
                    ######################################################
                    ### 【4】UPDATE PARTIALLY CLOSED ORDER
                    ######################################################                
    
                    if sub_order_incomplete:
                        ### add partially closed sell order child
                        self.update_partially_closed_order()
                        self.gvar.callers_method_name = 'stop_loss(0):'    
                   
                    ######################################################
                    ### 【5】CANCEL CLOSED BUY ORDER SUB
                    ######################################################
    
                    ### cancel closed buy order sub  
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}CANCEL{Bcolors.ENDC} closed buy order sub({sub_buy_order_id}) ") 
    
                    # increment buy cancel count
                    self.gvar.buy_cancel_count += 1   # cancel closed buy order (with sell order)            
                    self.coin.buy_cancel_count += 1   # cancel closed buy order (with sell order)
    
                    ### set a cancelled flag for buy order sub: PK(buy_time + buy_order_id) ★
                    find_mask = (buy_sub_df['buy_time'] == sub_buy_time) & (buy_sub_df['buy_order_id'] == sub_buy_order_id)
                    buy_sub_df.loc[find_mask, 'cancelled'] = True   
    
                    ######################################################
                    ### 【6】CANCEL OPEN/INCOMPLETE SELL ORDER SUB
                    ######################################################
    
                    ### for open or incomplete sell sub order     
    
                    # cancel open/incomplete sell order sub                                                    
                    status, msg_code, msg_str = self.api.exe_cancel_order(sub_sell_order_id)    # ★  
    
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}CANCEL{Bcolors.ENDC} open/incomplete sell order sub({sub_sell_order_id}): exe_cancel_order() ▶ {status=} ") 
    
                    sleep(0.5)    # sleep 0.5 sec
    
                    ###########################################################################################
                    ### 【7】RESELL OUTSTANDING SELL ORDER SUB QTY : EXECUTE SELL CLOSE ORDER MARKET (FAK)
                    ###########################################################################################
    
                    ### resell open/incomplete sell order sub with market price (stop loss price)
    
                    outstanding_qty = sub_qty - sub_real_qty                
                    new_sell_qty = outstanding_qty                                
                    qty = new_sell_qty
    
                    if sub_real_price > 0:
                        new_sell_price = sub_real_price + sub_real_fee                                                                                     
                    else:                    
                        new_sell_price = sub_price                                                                  
    
                    ### get the latest close price from the GMO master
    
                    # get a column position of the GMO master              
                    mas_latest_close_price = mas_df.iloc[-1]['close']   # get latest close price (GMO)              
          
                    status = 0            
                    loop_count = 0 
    
                    while True:
                        loop_count += 1    
    
                        ### resell sell order sub outstanding qty
                        status, res_df, msg_code, msg_str = self.api.exe_sell_close_order_market_wait(sub_position_id, qty, new_sell_price, mas_latest_close_price)  # ★ FAK(Fill and Kill): new_sell_price, mas_latest_close_price are used for fake mode ; 4BTC; 1BTC 
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}RESELL({loop_count}){Bcolors.ENDC} open/incomplete sell order sub({sub_sell_order_id}): exe_sell_close_order_market_wait() ▶ {status=} ") 
            
                        # error ?
                        if status != 0:
                            if status == 1 and msg_code == 'ERR-208':   # Exceeds the available balance
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC}{Bcolors.FAIL} the available balance exceeded error... ERR-208{Bcolors.ENDC} ")
                                self.debug.sound_alert(1)  
                                break   # exit while true loop and continue to read next sell order sub        
                            elif status == 8 and msg_code == 'ERR-888': # get_price() time out error ?
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} exe_sell_close_order_market_wait() time out error: ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                                break   # exit while true loop and continue to read next sell order sub     
                            else:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} exe_sell_close_order_market_wait() ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                                break   # exit while tue loop and continue to read next sell order sub  
                            # end of if status == 1 and msg_code == 'ERR-201':
                        # end of if status != 0:        
    
                        ### no error : res_df.shape[0] >= 1 (single or multiple rows)   
    
                        ######################################################################################################
                        ### 【8】ADD SELL ORDER CHILD ★ DO NOT CREATE RESELL VERSION OF NEW SELL ORDER SUB ★ 
                        ######################################################################################################
    
                        # res_df = get_price() response for exe_sell_order_market_wait()
                        # ----------------------------------------------------------------
                        #  #   Column       Dtype         
                        # ----------------------------------------------------------------         
                        #  0   executionId  int64         
                        #  1   fee          float64       
                        #  2   lossGain     float64                               
                        #  3   orderId      object(string)   
                        #  4   posiitonId   object(string)                          
                        #  5   price        float64  
                        #  6   settleType   object('OPEN' or 'CLOSE')  
                        #  7   side         object('BUY' or 'SELL')                  
                        #  8   size         float64           
                        #  9   symbol       object('BTC')                        
                        # 10   timestamp    datetime64[ns]     
                        # ----------------------------------------------------------------                   
    
                        ### IMPORTANT ★ DO NOT CREATE RESELL VERSION OF NEW SELL ORDER SUB ★ 
    
                        ### append a new record into the sell order child (★ this is the resell version of buy order ★)
    
                        # define sell order child list
                        order_id_list = []
                        position_id_list = []
                        real_qty_list = []
                        real_price_list = []
                        real_fee_list = []  
    
                        # response df for get_price for exe_sell_close_order_market() 
                        for k in range(len(res_df)):
                            order_id    = res_df.loc[k, 'orderId']          # orderId (same orderId): resell version of orderId
                            position_id = res_df.loc[k, 'positionId']       # positionId (same positionId)
                            real_qty    = res_df.loc[k, 'size']             # real qty : 
                            real_price  = res_df.loc[k, 'price']            # real price
                            real_fee    = res_df.loc[k, 'fee']              # real fee        
    
                            order_id_list.append(order_id)                  # S9999-999999-999999
                            position_id_list.append(position_id)            # P9999-999999-999999
                            real_qty_list.append(real_qty)
                            real_price_list.append(real_price)
                            real_fee_list.append(real_fee)
    
                            ### write a sell order child (resell version) ★              
                            # write_sell_order_child(sell_time: utcnow, sell_order_id: str, buy_time: utcnow, buy_order_id: str, position_id: str, qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:             
                            _qty_ = real_qty; _price_ = real_price
                            self.csv.write_sell_order_child(sell_time, order_id, buy_time, sub_buy_order_id, position_id, _qty_, _price_, real_qty, real_price, real_fee)    
                        # end of for k in range(len(res_df)):              
      
                        sum_real_qty = round(sum(real_qty_list), _q_)   # round(999.12, _q_)
                        sum_real_fee = round(sum(real_fee_list), 0)     # zero(0)          
    
                        if sum(real_price_list) > 0:
                            mean_real_price = round(statistics.mean(real_price_list), _p_) #  round(999999.123, _p_)
                        else:
                            mean_real_price = 0                       
    
                        sub_real_qty_list.append(sum_real_qty)                
                        sub_real_price_list.append(mean_real_price)
                        sub_real_fee_list.append(sum_real_fee)
    
                        ### check exit condition:      
    
                        sub_sum_real_qty = round(sum(sub_real_qty_list), _q_)     # round(9999.12, _q_)     
                        new_sub_sum_real_qty = sub_real_qty + sub_sum_real_qty    
                         
                        # ####################################################### TEMP TEMP TEMP
                        # temp_qty = round(sub_qty, _q_)
                        # temp_real_qty = round(new_sub_sum_real_qty, _q_)
                        # self.debug.trace_warn(f".... DEBUG: {temp_qty=:,.{_q_}f}, {temp_real_qty=:,.{_q_}f} ")
                        # ####################################################### TEMP TEMP TEMP  
    
                        if round(sub_qty, _q_) == round(new_sub_sum_real_qty, _q_):                                               
                            # increment sell cancel count
                            self.gvar.sell_cancel_count += 1  # cancel open/incomplete sell order            
                            self.coin.sell_cancel_count += 1  # cancel open/incomplete sell order
                            break   # exit while true loop 
                        else:       # outstanding buy_qty exists                    
                            pass    # skip (do nothing)                   
                        # end of round(sub_qty, digits) == round(new_sub_sum_real_qty, digits):
    
                        ### partial exe_sell_order_market_wait() = continue to sell remaining qty                        
                        remaining_qty = new_sell_qty - new_sub_sum_real_qty   
                        qty = remaining_qty
    
                        # continue exe_sell_close_order_market_wait() ★                                                             
                    # end of while True:                            
                      
                    #################################################################################
                    ### 【9】UPDATE SELL ORDER SUB (real_qty, real_price, real_fee, stop_loss)
                    #################################################################################
    
                    sub_sum_real_qty = round(sum(sub_real_qty_list), _q_)   # round(999.12,_q_)                                
                    sub_sum_real_fee = round(sum(sub_real_fee_list), 0)     # zero(0)
    
                    if sum(sub_real_price_list) > 0:
                        sub_mean_real_price = round(statistics.mean(sub_real_price_list), _p_)  # round(999999.123, _p_)
                    else:
                        sub_mean_real_price = 0                   
    
                    header_real_qty_list.append(sub_sum_real_qty)
                    header_real_price_list.append(sub_mean_real_price)
                    header_real_fee_list.append(sub_sum_real_fee)
    
                    ### update the sell order sub (real_qty, real_price, real_fee, stop_loss) ★
                    find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)
                    dfx = sell_sub_df[find_mask]
                    # not found sell order sub ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order sub => update
                        sell_sub_df.loc[find_mask, 'real_qty'] += sub_sum_real_qty      # add real_qty (resell version)                           
                        sell_sub_df.loc[find_mask, 'real_price'] = sub_mean_real_price  # update real_price (resell version)
                        sell_sub_df.loc[find_mask, 'real_fee'] += sub_sum_real_fee      # add real_fee (resell version)
                        sell_sub_df.loc[find_mask, 'stop_loss'] = True
                        ix = dfx.index[0]
                        new_sub_sum_real_qty = sell_sub_df.loc[ix, 'real_qty']
                        new_sub_sum_real_fee = sell_sub_df.loc[ix, 'real_fee']                    
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub(sell_order_id={sub_sell_order_id}, real_qty={new_sub_sum_real_qty:,.{_q_}f}, real_price={sub_mean_real_price:,.{_p_}f}, real_fee={new_sub_sum_real_fee:,.0f}, stop_loss=True) ")                     
                    # end of if dfx.shape[0] == 0:  
    
                    #################################################################################
                    ### 【10】UPDATE ORDER CSV FILE (stop_loss, order_closed)
                    #################################################################################
    
                    ### update order info (stop_loss=True, order_closed) ★        
                    # update_order_csv_buy(buy_time: utcnow, buy_order_id: str, col_name_list: list, col_value_list: list) -> None: # PK(buy_time + buy_order_id)
                    self.csv.update_order_csv_buy(sub_buy_time, sub_buy_order_id, col_name_list=['stop_loss'], col_value_list=[True])                                                                               
    
                    # reset sell order sub list values
                    sub_real_qty_list = []
                    sub_real_price_list = []
                    sub_real_fee_list = [] 
    
                    # continue to next sell order sub... ★ 
                # end of for j in range(len(sell_sub_df)):
    
                #################################################################################
                ### 【11】SAVE BUY/SELL ORDER SUB TO CSV FILES
                #################################################################################
    
                ### CLOSE (save the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
                csv_file = self.folder_trading_type + f'buy_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/buy_order_sub(BTC_JPY).csv  
                buy_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode                
                            
                ### CLOSE (save the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
                csv_file = self.folder_trading_type + f'sell_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/sell_order_sub(BTC_JPY).csv  
                sell_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode                                              
    
                #################################################################################
                ### 【12】UPDATE SELL ORDER (real_qty, real_price, real_fee)
                #################################################################################
    
                if len(header_real_qty_list) > 0:
                    ### update sell order (real_qty, real_price, real_fee) ★
                    header_sum_real_qty = round(sum(header_real_qty_list), _q_)     # round(999.12, _q_        
                    header_sum_real_fee = round(sum(header_real_fee_list), 0)       # zero(0)            
                    
                    if sum(header_real_price_list) > 0:
                        header_mean_real_price = round(statistics.mean(header_real_price_list), _p_)    # round(999999.123, _p_)
                    else:
                        header_mean_real_price = 0            
    
                    find_mask = sell_df['sell_time'] == sell_time
                    dfx = sell_df[find_mask]
                    # not found sell order ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order({sell_time:%Y-%m-%d %H:%M:%S}) ▶ {Bcolors.FAIL} order({sell_time=:%Y-%m-%d %H:%M:%S}) not found {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order header => update
                        sell_df.loc[find_mask, 'real_qty'] += header_sum_real_qty       # add real_qty (resell version)                          
                        sell_df.loc[find_mask, 'real_price'] = header_mean_real_price   # update real_price (resell version) 
                        sell_df.loc[find_mask, 'real_fee'] += header_sum_real_fee       # add real_fee (resell version)
                        ix = dfx.index[0]
                        new_header_sum_real_qty = sell_df.loc[ix, 'real_qty'] 
                        new_header_sum_real_fee = sell_df.loc[ix, 'real_fee']
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={new_header_sum_real_qty:,.{_q_}f}, real_price={header_mean_real_price:,.{_p_}f}, real_fee={new_header_sum_real_fee:,.0f}) ") 
                    # end of if dfx.shape[0] == 0:                   
                # end of if len(header_real_qty_list) > 0:
    
                # reset list values
                header_real_qty_list = []
                header_real_price_list = []
                header_real_fee_list = []     
    
                # continue to next sell order 
            # end of for i in range(len(sell_df)):
    
            #################################################################################
            ### 【13】SAVE BUY/SELL ORDER TO CSV FILES 
            #################################################################################
    
            ### CLOSE (save the buy order csv file) : buy_order(BTC_JPY).csv         
            csv_file = self.folder_trading_type + f'buy_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/buy_order(BTC_JPY).csv  
            buy_df.to_csv(csv_file, header=False, index=False) # OverWrite mode  
    
            ### CLOSE (save the sell order csv file) : sell_order(BTC_JPY).csv      
            csv_file = self.folder_trading_type + f'sell_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/sell_order(BTC_JPY).csv   
            sell_df.to_csv(csv_file, header=False, index=False) # OverWrite mode        
    
            return
    

    click image to zoom!
    図8-1
    図8-1はデモプログラムの実行結果です。 画面にはBuySellクラスのstop_loss(0)メソッドのログが表示されています。 stop_loss(0)では未執行分の売り注文のキャンセルが実行されています。 さらに未執行の売りボジションに対して成行きの売り注文を出して強制的に決済しています。 詳細は図8-2で説明します。


    click image to zoom!
    図8-2
    図8-2は売り注文で作成されたCSVファイルをExcelで開いた画像です。 売り注文のCSVファイルも買い注文と同様3階層になっています。 ここでは3種類のCSVファイルを開いています。 「sell_order(XRP_JPY).csv」ファイルには「sell_time, qty, price, real_qry, real_price,...」などが格納されています。 「real_qty, real_price」には約定した数量と価格(レート)が格納されます。

    「sell_order_sub(XRP_JPY).csv」ファイルには「sell_time, sell_order_id, position_id, real_qty, real_price, stop_loss,...」などが格納されています。 「real_qty」には約定した数量(40)が格納されています。 「stop_loss」には「TRUE」が格納されているのでストップ・ロスが適用されています。

    「sell_order_child(XRP_JPY).csv」ファイルには「sell_time, sell_order_id, position_id, real_qty, real_price,...」などが格納されています。 実はこの売り注文は数量「40」で出されていますが、数量が「20」だけ執行された状態でストップ・ロスが適用されています。 この場合、未執行の数量「20」に対してキャンセル処理を行います。 さらに、未執行の売りポジションに対して成行きの売り注文を出して残数の「20」を強制的に決済します。 つまり、ストップ・ロスを適用します。

    「sell_order_child(XRP_JPY).csv」のExcelシートの行2は取引所で部分的に執行された売り注文のデータで、 行3はストップ・ロスで強制的に決済したときの売り注文のデータです。 したがってシートの列(B)の「sell_order_id」には異なる売りの注文IDが格納されています。 余談ですが、リアルで取引している状態でこのような条件を検証するのは無理です。 ところが、BOTのシミュレーション機能を使えば簡単に今回のような処理の検証ができます。


    click image to zoom!
    図8-3
    図8-3は損益ファイルをExcelで開いた画像です。 「order(XRP_JPY).xlsx」ファイルには「time(B/S), order(B/S), qty(B/S), price(B/S), profit(AMT), profit(%), closed, stop, cancel,..」などの情報が格納されています。 「()」内のBはBuy, SはSellの意味です。 「stop」に「TRUE」が格納されているので、この取引にはストップ・ロスが適用されたことが分かります。 「qty」に執行された数量「40」が格納されていますが、これだと全数ストップ・ロスが適用されたのか、 部分的にストップ・ロスが適用されたのか分かりません。 このような場合は、図8-2の「sell_order_sub(XRP_JPY).csv」と「sell_order_child(XRP_JPY).csv」ファイルを参照します。


  9. BuySellクラスにcancel_pending_order()メソッドを追加する

    ここではBuySellクラスにcancel_pending_order()メソッドを追加します。 このメソッドは、保留状態になっているポジションを強制的に決済してレバレッジ手数料が加算されるのを回避するときに使用します。 GMOコインの場合、レバレッジ手数料は午前6時を経過したときに加算されます。 したがって、レバレッジ手数料を回避するには午前6時前にポジションを決済させる必要があります。

    buy_sell.py:
        ########################################    
        def cancel_pending_order(self) -> None: 
            """        
    
            """         
    
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal
            self.gvar.callers_method_name = 'cancel_pending_order():'
            self.debug.trace_write(f".... {self.gvar.callers_method_name} ")  
    
            df = self.coin.master2_df              
            mas_df = pd.DataFrame(df)   # cast df (master dataframe)
          
            ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)  
              
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
    
            #################################################
            ### 【1】READ ALL SELL ORDERs 
            #################################################
    
            # define sell order list
            header_real_qty_list = []
            header_real_price_list = []
            header_real_fee_list = []        
    
            ### get all sell order 
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']   # ★ unique time                 
                buy_time        = sell_df.loc[i, 'buy_time']    # ★ unique time                       
                sell_qty        = sell_df.loc[i, 'qty']
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True         
            
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell order sub
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell order sub ?
                    if dfx.shape[0] > 0:
                        ix = dfx.index[0]                
                        sell_order_id = dfx.loc[ix, 'sell_order_id']    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake order sub({sell_order_id})... ")
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_order_id}) has been closed: {sell_order_closed=} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_time:%Y-%m-%d %H:%M:%S}) has been closed: {sell_order_closed=} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # skip closed order    
                # end of if sell_order_closed:  
    
                #################################################################
                ### 【2】READ ALL SELL ORDER SUB (OPEN OR INCOMPLETE STATUS)
                #################################################################
    
                ### for sell order sub (open or incomplete status)
    
                # define sell order sub list
                sub_real_qty_list = []
                sub_real_price_list = []
                sub_real_fee_list = [] 
    
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order
                for j in range(len(sell_subx_df)):   # ★ sell_subx_df ★
                    ### get sell order sub info
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # ★ 'S9999-999999-999999' (unique id)           
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # ★ 'B9999-999999-999999' (non unique id) : sell sub(many) => buy_sub(one)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # ★ (unique id)
                    sub_qty             = sell_subx_df.loc[j, 'qty']                                  
                    sub_price           = sell_subx_df.loc[j, 'price']               
                    sub_real_qty        = sell_subx_df.loc[j, 'real_qty']                        
                    sub_real_price      = sell_subx_df.loc[j, 'real_price']            
                    sub_real_fee        = sell_subx_df.loc[j, 'real_fee']                  
                    sub_stop_loss       = sell_subx_df.loc[j, 'stop_loss']
                    sub_cancelled       = sell_subx_df.loc[j, 'cancelled']                
            
                    sub_order_open = False
                    sub_order_closed = False
                    sub_order_incomplete = False
    
                    if sub_real_qty == 0:
                        sub_order_open = True
                    elif sub_qty == sub_real_qty:
                        sub_order_closed = True
                    else:
                        sub_order_incomplete = True                             
    
                    # fake sell order sub ? => SKIP    
                    if sub_sell_order_id.startswith('Fake') :
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id}) ") 
                        continue               
    
                    # sell order sub has been stopped loss ? => SKIP
                    if sub_stop_loss: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss ▶ {sub_stop_loss=} ")  
                        continue
    
                    # cancelled sell order sub ? => SKIP    
                    if sub_cancelled: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled: {sub_cancelled=} ") 
                        continue           
    
                    # closed sell order sub ? => SKIP    
                    if sub_order_closed: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed: {sub_order_closed=} ") 
                        continue  
    
                    ### open or incomplete sell order sub    
    
                    ######################################################
                    ### 【3】UPDATE PARTIALLY CLOSED ORDER
                    ######################################################                
    
                    if sub_order_incomplete:
                        ### add partially closed sell order child
                        self.update_partially_closed_order()
                        self.gvar.callers_method_name = 'cancel_pending_order():'                
    
                    ##########################################################################################
                    ### 【4】CANCEL CLOSED BUY ORDER SUB : ★ Sell Order Sub(many) => Buy Order Sub(one) ★
                    ##########################################################################################
    
                    ### for closed buy order sub                             
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}CANCEL{Bcolors.ENDC} closed buy order sub({sub_buy_order_id}) ") 
    
                    # increment buy cancel count
                    self.gvar.buy_cancel_count += 1   # cancel closed buy order             
                    self.coin.buy_cancel_count += 1   # cancel closed buy order 
    
                    ### set a cancelled flag for buy order sub : ★ PK(buy_time + buy_order_id) ★
                    find_mask = (buy_sub_df['buy_time'] == sub_buy_time) & (buy_sub_df['buy_order_id'] == sub_buy_order_id)
                    buy_sub_df.loc[find_mask, 'cancelled'] = True                
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} update_buy_order_sub(buy_order_id={sub_buy_order_id}, cancelled=True) ")                                                              
                                       
                    ######################################################
                    ### 【5】CANCEL OPEN/INCOMPLETE SELL ORDER SUB
                    ######################################################
    
                    ### cancel open or incomplete sell order sub  
    
                    ### cancel sell order sub                    
                    status, msg_code, msg_str = self.api.exe_cancel_order(sub_sell_order_id)    # ★        
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}CANCEL{Bcolors.ENDC} open/incomplete sell order sub({sub_sell_order_id}): exe_cancel_order() ▶ {status=} ")   
                    sleep(0.5)  # sleep 0.5 sec                       
    
                    ##################################################################################################
                    ### 【6】RESELL OUTSTANDING SELL ORDER SUB QTY : EXECUTE SELL CLOSE ORDER MARKET WAIT (FAK)
                    ##################################################################################################
    
                    ### resell open or incomplete sell order sub with market price 
    
                    outstanding_qty = sub_qty - sub_real_qty    # calculate outstanding qty 
                    new_sell_qty = outstanding_qty                                  
                    qty = new_sell_qty   
    
                    if sub_real_price > 0:
                        new_sell_price = sub_real_price + sub_real_fee                              
                    else:
                        new_sell_price = sub_price
    
                    ### get the latest close price from the GMO master
    
                    # get a column position of the GMO master             
                    mas_latest_close_price = mas_df.iloc[-1]['close']                                 # get latest close price (GMO)                         
    
                    status = 0
                    loop_count = 0
    
                    while True:
                        loop_count += 1    
       
                        # exe_sell_close_order_market_wait(position_id: str, qty: float, price: float, latest_close_price=0.0) -> tuple:     #  return (status, df, msg_code, msg_str) 
                        status, res_df, msg_code, msg_str = self.api.exe_sell_close_order_market_wait(sub_position_id, qty, new_sell_price, mas_latest_close_price)  # ★ FAK(Fill and Kill): new_sell_price_str, mas_latest_close_price_str ares used for fake mode ; 4BTC; 1BTC               
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}RESELL({loop_count}){Bcolors.ENDC} open/incomplete sell order sub({sub_sell_order_id}): exe_sell_close_order_market_wait() ▶ {status=} ") 
            
                        # error ?
                        if status != 0:
                            if status == 1 and msg_code == 'ERR-208':   # Exceeds the available balance
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC}{Bcolors.FAIL} the available balance exceeded error... ERR-208{Bcolors.ENDC} ")
                                self.debug.sound_alert(1)  
                                break   # exit while true loop and continue to read next sell sub orders                                  
                            elif status == 8 and msg_code == 'ERR-888': # get_price() time out error ?
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} exe_sell_close_order_market_wait() time out error: ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                                break   # exit while true loop and continue to read next sell sub orders 
                            else: # misc error
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} exe_sell_close_order_market_wait() misc error ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                                break   # exit while true loop and continue to read next sell order sub                    
                            # end of if status == 1 and msg_code == 'ERR-201':
                        # end of if status != 0:        
    
                        ### no error : res_df.shape[0] >= 1 (single or multiple rows)
    
                        ##################################################################################################
                        ### 【7】ADD SELL ORDER CHILD ★ DO NOT CREATE RESELL VERSION OF NEW SELL ORDER SUB ★ 
                        ##################################################################################################
    
                        # res_df = response for exe_sell_close_order_market_wait() 
                        # -----------------------------------------------------------
                        #  #   Column       Dtype         
                        # -----------------------------------------------------------         
                        #  0   executionId  int64         
                        #  1   fee          float64       
                        #  2   lossGain     float64                               
                        #  3   orderId      object(string)  (non unique)  
                        #  4   positionId   object(string)  (non unique)     
                        #  5   price        float64  
                        #  6   settleType   object('OPEN' or 'CLOSE')  
                        #  7   side         object('BUY' or 'SELL')                  
                        #  8   size         float64           
                        #  9   symbol       object('BTC_JPY')                        
                        # 10   timestamp    datetime64[ns]  
                        # ----------------------------------------------------------
    
                        ### IMPORTANT ★ DO NOT CREATE RESELL VERSION OF NEW SELL ORDER SUB ★ 
                        
                        ### append a new record into the sell order child  (★ this is the resell version of sell order child ★)
    
                        # define sell order child list
                        order_id_list = []
                        position_id_list = []
                        real_qty_list = []
                        real_price_list = []
                        real_fee_list = []   
    
                        # response for exe_sell_close_order_market_wait()
                        for k in range(len(res_df)):
                            order_id    = res_df.loc[k, 'orderId']          # orderId (same orderId) : resell version of orderId         
                            position_id = res_df.loc[k, 'positionId']       # positionId (same positionId)               
                            real_qty    = res_df.loc[k, 'size']             # real qty     
                            real_price  = res_df.loc[k, 'price']            # real price
                            real_fee    = res_df.loc[k, 'fee']              # real fee
    
                            order_id_list.append(order_id)                  # S9999-999999-999999
                            position_id_list.append(position_id)            # P9999-999999-999999
                            real_qty_list.append(real_qty)                  # 
                            real_price_list.append(real_price)
                            real_fee_list.append(real_fee)
    
                            ### write a sell order child (resell version) ★   
                            # write_sell_order_child(sell_time: utcnow, sell_order_id: str, buy_time: utcnow, buy_order_id: str, position_id: str, qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None: 
                            _qty_ = real_qty; _price_ = real_price
                            self.csv.write_sell_order_child(sell_time, order_id, buy_time, sub_buy_order_id, position_id, _qty_, _price_, real_qty, real_price, real_fee)                             
                        # end of for k in range(len(res_df)):                      
    
                        sum_real_qty = round(sum(real_qty_list), _q_)                           # round(999.12, _q_)                                 
                        sum_real_fee = round(sum(real_fee_list), 0)                             # zero(0)
    
                        if sum(real_price_list) > 0:
                            mean_real_price = round(statistics.mean(real_price_list), _p_)      # round(999999.123, _p_)
                        else:
                            mean_real_price = 0    
    
                        sub_real_qty_list.append(sum_real_qty)                                  
                        sub_real_price_list.append(mean_real_price)
                        sub_real_fee_list.append(sum_real_fee)
                                          
                        ### check exit condition:      
                         
                        sub_sum_real_qty = round(sum(sub_real_qty_list), _q_)                             
                        new_sub_sum_real_qty = sub_real_qty + sub_sum_real_qty                  
                
                        # no outstanding sell_qty ?
                        if round(sub_qty, _q_) == round(new_sub_sum_real_qty, _q_):            
                            # increment sell cancel count
                            self.gvar.sell_cancel_count += 1  # cancel open/incomplete sell order            
                            self.coin.sell_cancel_count += 1  # cancel open/incomplete sell order
                            break   # exit while true loop
                        else:       # outstanding sell_qty exists
                            pass    # skip (do nothing)                 
                        # end of if round(sub_qty, digits) == round(new_sub_sum_real_qty, digits):
    
                        ### partial exe_sell_close_order_market_wait() = continue to sell remaining qty
                            
                        remaining_qty = new_sell_qty - new_sub_sum_real_qty    
                        qty = remaining_qty
    
                        # continue exe_sell_close_order_market_wait() ★
                    # end of while True:   
                     
                    #################################################################################
                    ### 【8】UPDATE SELL ORDER SUB (real_qty, real_price, real_fee, cancelled)
                    #################################################################################                                  
        
                    sub_sum_real_qty = round(sum(sub_real_qty_list), _q_)                                                     
                    sub_sum_real_fee = round(sum(sub_real_fee_list), 0)                    
    
                    if sum(sub_real_price_list) > 0:
                        sub_mean_real_price = round(statistics.mean(sub_real_price_list), _p_) 
                    else:
                        sub_mean_real_price = 0                      
    
                    header_real_qty_list.append(sub_sum_real_qty)                          
                    header_real_price_list.append(sub_mean_real_price)
                    header_real_fee_list.append(sub_sum_real_fee)
    
                    ### update the sell order sub (real_qty, real_price, real_fee, cancelled) 
                    find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)
                    dfx = sell_sub_df[find_mask]
                    # not found sell order sub ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL}sell order sub not found {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order sub => update 
                        sell_sub_df.loc[find_mask, 'real_qty'] += sub_sum_real_qty      # add real_qty (resell version)                                  
                        sell_sub_df.loc[find_mask, 'real_price'] = sub_mean_real_price  # update real_price (resell version)
                        sell_sub_df.loc[find_mask, 'real_fee'] += sub_sum_real_fee      # add real_fee (resell version)
                        sell_sub_df.loc[find_mask, 'cancelled'] = True
                        ix = dfx.index[0]
                        new_sub_sum_real_qty = sell_sub_df.loc[ix, 'real_qty']         
                        new_sub_sum_real_fee = sell_sub_df.loc[ix, 'real_fee']
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub(sell_order_id={sub_sell_order_id}, real_qty={new_sub_sum_real_qty:,.{_q_}f}, real_price={sub_mean_real_price:,.{_p_}f}, real_fee={new_sub_sum_real_fee:,.0f}, cancelled=True) ")                                    
                    # end of if dfx.shape[0] == 0:   
                        
                    #################################################################################
                    ### 【9】UPDATE ORDER CSV FILE (cancelled, order_closed)
                    #################################################################################                        
                        
                    ### update order csv file (cancelled=True, order_closed=True)  ★  
                    # update_order_info_buy(buy_time, buy_order_id, symbol='BTC_JPY', col_name=any, col_value=any):   
                    # update_order_csv_buy(buy_time: utcnow, buy_order_id: str, col_name_list: list, col_value_list: list) -> None: # PK(buy_time + buy_order_id)
                    self.csv.update_order_csv_buy(sub_buy_time, sub_buy_order_id, col_name_list=['cancelled'], col_value_list=[True]) # PK(buy_time + buy_order_id)                                                                                                                       
    
                    # reset sell order sub list values
                    sub_real_qty_list = []
                    sub_real_price_list = []
                    sub_real_fee_list = [] 
    
                    # continue to next sell order sub
                # end of for j in range(len(sell_subx_df)):     
    
                #################################################################################
                ### 【10】SAVE BUY/SELL ORDER SUB TO CSV FILES
                #################################################################################
    
                ### CLOSE (save the buy order sub csv file)
                csv_file = self.folder_trading_type + f'buy_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/buy_order_sub(BTC_JPY).csv  
                buy_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode   
                                         
                ### CLOSE (save the sell order sub csv file) 
                csv_file = self.folder_trading_type + f'sell_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/sell_order_sub(BTC_JPY).csv  
                sell_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode                                             
    
                #################################################################################
                ### 【11】UPDATE SELL ORDER (real_qty, real_price, real_fee)
                #################################################################################
                    
                if len(header_real_qty_list) > 0:    
                    ### update sell order (real_qty, real_price, real_fee) ★
                    header_sum_real_qty = round(sum(header_real_qty_list), _q_)                      
                    header_sum_real_fee = round(sum(header_real_fee_list), 0)                            
                    
                    if sum(header_real_price_list) > 0:
                        header_mean_real_price = round(statistics.mean(header_real_price_list), _p_)  
                    else:
                        header_mean_real_price = 0    
    
                    find_mask = sell_df['sell_time'] == sell_time
                    dfx = sell_df[find_mask]
                    # not found sell order ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL}sell order({sell_time=:%Y-%m-%d %H:%M:%S}) not found {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order => update
                        sell_df.loc[find_mask, 'real_qty'] += header_sum_real_qty     # add real_qty (resell version)                            
                        sell_df.loc[find_mask, 'real_price'] = header_mean_real_price # update real_price (resell version) 
                        sell_df.loc[find_mask, 'real_fee'] += header_sum_real_fee     # add real_fee (resell version) 
                        ix = dfx.index[0]
                        new_header_sum_real_qty = sell_df.loc[ix, 'real_qty'] 
                        new_header_sum_real_fee = sell_df.loc[ix, 'real_fee']                
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={new_header_sum_real_qty:,.{_q_}f}, real_price={header_mean_real_price:,.{_p_}f}, real_fee={new_header_sum_real_fee:,.0f}) ")                                                    
                    # end of if dfx.shape[0] == 0:          
                # end of if len(header_real_qty_list) > 0:
    
                # reset sell header list values
                header_real_qty_list = []
                header_real_price_list = []
                header_real_fee_list = []     
    
                # continue to next sell order... ★              
            # end of for i in range(len(sell_df)):  
    
            #################################################################################
            ### 【12】SAVE BUY/SELL ORDER TO CSV FILES 
            #################################################################################        
    
            ### CLOSE (save the buy order csv file)          
            csv_file = self.folder_trading_type + f'buy_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/buy_order(BTC_JPY).csv  
            buy_df.to_csv(csv_file, header=False, index=False) # OverWrite mode  
    
            ### CLOSE (save the sell order csv file)       
            csv_file = self.folder_trading_type + f'sell_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/sell_order(BTC_JPY).csv   
            sell_df.to_csv(csv_file, header=False, index=False) # OverWrite mode 
    
            return    
    

    click image to zoom!
    図9-1
    図9-1はデモプログラムの実行結果です。 画面にはBuySellクラスのcancel_pending_order()メソッドが実行されたログが表示されています。 このメソッドは後述するレバレッジ手数料を発生させたくない場合などに使用します。 GMOコインはポジションを保持した状態で午前6時を経過するとレバレッジ手数料を加算します。 BOTはこのレバレッジ手数料を回避するために強制的にポジションを決済する機能をサポートしています。

    ポジションを強制的に決済させるには、 Gvarクラスのcancel_pending_ordersプロパティに「True」を設定して自動トレードします。


    click image to zoom!
    図9-2
    図9-2は売りのポジションを強制的に決済したときに作成されたCSVファイルと損益ファイルをExcelで表示した画面です。 「sell_order_sub(XRP_JPY).csv」ファイルの「real_qty」には売りの数量「40XRP_JPY」が格納されています。 「sell_order_child(XRP_JPY).csv」ファイルの「real_qty」には執行された数量が「20XRP_JPY」に分割されて格納されています。 Excelシートの行2(上段)はGMOコインが部分的に執行した数量で、 行3(下段)はBOTが強制的に残数「20XRP_JPY」を決済したときの数量です。

    ポジションが強制的に決済されたかどうか調べるには、 まず「sell_order_sub(XRP_JPY).csv」の「cancelled」の値をチェックします。 「TRUE」が格納されていれば強制的に決済されています。 この場合、「sell_order_child(XRP_JPY).csv」の「sell_order_id」に異なる売り注文IDが格納されます。


  10. BuySellクラスにclose_pending_order()メソッドを追加する

    ここではBuySellクラスにclose_pending_order()メソッドを追加します。 このメソッドはレバレッジ手数料を計算させたいときに使用します。 BOTを使用するときは、午前5時~午前6時の時間帯に自動的にこのメソッドが呼ばれます。 close_pending_order()はポジションを保留している注文のレバレッジ手数料を計算して損益ファイルを更新します。

    buy_sell.py:
        ####################################### 
        def close_pending_order(self) -> None:
            """        
    
            """        
    
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal
            self.gvar.callers_method_name = 'close_pending_order():'
            self.debug.trace_write(f".... {self.gvar.callers_method_name} ")  
    
            # df = self.coin.master2_df      
            # mas_df = pd.DataFrame(df)   # cast df (master dataframe)
    
            ### OPEN (load the order csv file) : order(BTC_JPY).csv 
            ord_df = self.csv.get_order_csv()          
          
            # ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            # buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            # ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            # buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)  
    
            ### OPEN (load the buy order child log csv file) : buy_order_child(BTC_JPY).csv 
            buy_ch_df = self.csv.get_buy_order_child() # PK(buy_time + buy_order_id + position_id)         
              
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
    
            # ### OPEN (load the sell order child csv file) : sell_order_child(BTC_JPY).csv 
            # sell_ch_df = self.csv.get_sell_order_child() # PK(sell_time + sell_order_id + position_id)          
    
            #################################################
            ### 【1】READ ALL SELL ORDER 
            #################################################
         
            ### get all sell orders 
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']   # ★ unique time                 
                buy_time        = sell_df.loc[i, 'buy_time']    # ★ unique time                       
                sell_qty        = sell_df.loc[i, 'qty']
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True         
            
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell sub order log
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell sub order ?
                    if dfx.shape[0] > 0:
                        ix = dfx.index[0]                
                        sell_order_id = dfx.loc[ix, 'sell_order_id']    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sell_order_id})... ")
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_order_id}) has been closed: {sell_order_closed=} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_time=:%Y-%m-%d %H:%M:%S}) has been closed: {sell_order_closed=} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # skip closed sell order    
                # end of if sell_order_closed:  
    
                #################################################################
                ### 【2】READ ALL SELL ORDER SUB (OPEN OR INCOMPLETE STATUS)
                #################################################################
    
                ### for sell order sub (open or incomplete status)
    
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order
                for j in range(len(sell_subx_df)):   # ★ sell_subx_df ★
                    ### get sell order sub info
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # ★ 'S9999-999999-999999' (unique id)           
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # ★ 'B9999-999999-999999' (non unique id) : sell sub(many) => buy_sub(one)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # ★ (unique id)
                    sub_qty             = sell_subx_df.loc[j, 'qty']                                  
                    sub_price           = sell_subx_df.loc[j, 'price']               
                    sub_real_qty        = sell_subx_df.loc[j, 'real_qty']                        
                    sub_real_price      = sell_subx_df.loc[j, 'real_price']            
                    sub_real_fee        = sell_subx_df.loc[j, 'real_fee']  
                    sub_get_price_count = sell_subx_df.loc[j, 'get_price_count']                
                    sub_stop_loss       = sell_subx_df.loc[j, 'stop_loss']
                    sub_cancelled       = sell_subx_df.loc[j, 'cancelled']                
            
                    sub_order_open = False
                    sub_order_closed = False
                    sub_order_incomplete = False
    
                    if sub_real_qty == 0:
                        sub_order_open = True
                    elif sub_qty == sub_real_qty:
                        sub_order_closed = True
                    else:
                        sub_order_incomplete = True                             
    
                    # fake sell order sub ? => SKIP    
                    if sub_sell_order_id.startswith('Fake') :
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id}) ") 
                        continue               
    
                    # sell order sub has been stopped loss ? => SKIP
                    if sub_stop_loss: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss ▶ {sub_stop_loss=} ")  
                        continue
    
                    # cancelled sell order sub ? => SKIP    
                    if sub_cancelled: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled: {sub_cancelled=} ") 
                        continue           
    
                    # closed sell order sub ? => SKIP    
                    if sub_order_closed: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed: {sub_order_closed=} ") 
                        continue  
    
                    ### open or incomplete sell order sub
                     
                    #################################################################
                    ### 【3】CALCULATE LEVERAGE FEE AND UPDATE ORDER CSV FILE
                    #################################################################
    
                    ### calculate leverage fee 
    
                    # get buy order child 
    
                    find_mask = (buy_ch_df['buy_time'] == sub_buy_time) & (buy_ch_df['buy_order_id'] == sub_buy_order_id) & (buy_ch_df['position_id'] == sub_position_id) 
                    dfx = buy_ch_df[find_mask]
                    # not found buy order child ?
                    if dfx.shape[0] == 0:
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}not found buy order child {Bcolors.ENDC} : {sub_buy_time=:%Y-%m-%d %H:%M:%S}, {sub_buy_order_id=}, {sub_position_id=}  ") 
                    else: # found buy order child 
                        ix = dfx.index[0]
                        ch_buy_real_qty = dfx.loc[ix, 'real_qty']
                        ch_buy_real_price = dfx.loc[ix, 'real_price']
                        ch_leverage_fee = math.ceil((ch_buy_real_price * ch_buy_real_qty) * 0.0004) # leverage fee (0.04%)
    
                        ### update order csv file (accumulated_leverage_fee, close_count, close_update_date)
    
                        # not empty order csv file ?
                        if ord_df.shape[0] > 0:
                            find_mask = ord_df['buy_order_id'] == sub_buy_order_id  # B9999-999999-999999
                            dfx = ord_df[find_mask]
                            # not found order csv file ?
                            if dfx.shape[0] == 0:   
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}not found order csv file{Bcolors.ENDC} : {sub_buy_order_id=}  ") 
                            else: # found order csv file 
                                update_order_csv = False
    
                                ix = dfx.index[0]
                                close_count = ord_df.loc[ix, 'close_count']
                                close_update_date = ord_df.loc[ix, 'close_update_date']
                                accumulated_leverage_fee = ord_df.loc[ix, 'accumulated_leverage_fee']
                                
                                if close_count == 0:
                                    update_order_csv = True
                                    # self.debug.trace_warn(f"DEBUG: update_order_csv={update_order_csv}, close_count={close_count} ")  
                                else: # close_count > 0
                                    now_date_str = f"{dt.datetime.now():%Y%m%d}"            # YYYYMMDD    
                                    close_update_date_str = f"{close_update_date:%Y%m%d}"   # YYYYMMDD                                
                                    if close_update_date_str != now_date_str:
                                        update_order_csv = True
                                    # self.debug.trace_warn(f"DEBUG: update_order_csv={update_order_csv}, close_count={close_count}, now_date_str={now_date_str}, close_update_date_str={close_update_date_str}, leverage_fee={accumulated_leverage_fee:,.0f} ")    
                                    # update_order_csv = True  # DEBUG DEBUG DEBUG
                                # end of if close_count == 0:
    
                                if update_order_csv:
                                    # update order csv file 
                                    ord_df.loc[find_mask, 'accumulated_leverage_fee'] += ch_leverage_fee      
                                    ord_df.loc[find_mask, 'close_count'] += 1
                                    ord_df.loc[find_mask, 'close_update_date'] = dt.datetime.now()    
                                    ord_df.loc[find_mask, 'log_time'] = dt.datetime.now() 
    
                                    ord_df['close_update_date'] = pd.to_datetime(ord_df['close_update_date']) # object => ns time (jp time)  
                                    new_accumulated_leverage_fee = ord_df.loc[ix, 'accumulated_leverage_fee']
                                    new_close_count = ord_df.loc[ix, 'close_count']                               
                                    new_close_update_date = ord_df.loc[ix, 'close_update_date']
                                    self.debug.trace_write(f".... {self.gvar.callers_method_name} update_order_csv_buy(): {sub_buy_order_id=}, {new_accumulated_leverage_fee=:,.0f}, {new_close_count=}, {new_close_update_date=:%Y-%m-%d %H:%M:%S} ") 
                        
                        # end of if ord_df.shape[0] > 0:
                    # end of if dfx.shape[0] == 0:                 
                     
                    # continue to next sell order sub...
                # end of for j in range(len(sell_subx_df)):                                 
    
                # continue to next sell order... ★              
            # end of for i in range(len(sell_df)):  
    
            #################################################################################
            ### 【4】SAVE ORDER CSV TO CSV FILE 
            #################################################################################        
    
            ### CLOSE (save to the order csv file) : order(BTC_JPY).csv
            if ord_df.shape[0] > 0:
                ### CLOSE (order csv file)
                csv_file = self.folder_trading_type + f'order({symbol}).csv'  
                # desktop-pc/bot/buy_sell/order(BTC_JPY).csv   
                ord_df.to_csv(csv_file, header=False, index=False) # overWrite the order file  
    
            return
    
    

    click image to zoom!
    図10-1
    図10-1はデモプログラムの実行結果です。 画面にはclose_pending_order()メソッドが実行されたことがログに表示されています。


    click image to zoom!
    図10-2
    図10-2はCSVファイルと損益ファイルをExcelで開いたときの画面です。 「buy_order_child(XRP_JPY).csv」ファイルの「real_qty」には買い注文で執行された数量「40XRP_JPY」が格納されています。 「sell_order_sub(XRP_JPY).csv」ファイルの「real_qty」には売り注文で執行された数量「20XRP_JPY」が格納されています。 売り注文の残数「20XRP_JPY」があるので、GMOコインは午前6時を経過するとレバレッジ手数料を加算します。

    「order(XRP_JPY).xlsx」ファイルにはこのレバレッジ手数料が自動的に計算されて格納されます。 Excelのシート「fee(L)」にはレバレッジ手数料の累計が格納されます。 「count(L)」にはレバレッジ手数料が加算された回数が格納されます。 「close(L)」にはレバレッジ手数料が加算された最終日時が格納されます。 ちなみに(L)は「Leverage」の略です。 「profit(AMT)」から「fee(L)」を減算すると純利益が計算できます。


  11. BuySellクラスの全てのソースコードを掲載

    ここではBuySellクラスの全てのソースコードを掲載しています。

    buy_sell.py:
    
    # buy_sell.py
    
    """
    BuySell class
    """
    
    # import the libraries
    from time import sleep  
    import datetime as dt
    from datetime import timedelta
    
    import math
    import statistics
    from decimal import Decimal
    
    import numpy as np
    import pandas as pd
    
    from lib.base import Bcolors, Gvar, Coin, Gmo_dataframe 
    from lib.debug import Debug
    from lib.api import Api
    from lib.csv import Csv
    from lib.trade import Trade    
    
    import warnings
    warnings.simplefilter('ignore')
    
    ##############
    class BuySell:
        """  
        __init__(), __str__(),     
        reset_restart_process(), 
        buy_sell(), buy_sell_algorithm_0(), buy_sell_algorithm_1, buy_sell_algorithm_2(), buy_sell_algorithm_3(),...
        create_buy_order(), create_sell_order()
        update_order(), update_closed_order(), update_partially_closed_order(), update_order_profit() 
        stop_loss(), stop_loss_0(), stop_loss_1(),...
        cancel_pending_order()
        close_pending_order()
        """
         
        ######################################################
        def __init__(self, gvar: object, coin: object) ->None: 
            self.gvar = gvar                            # Gvar
            self.coin = coin                            # Coin       
            self.debug = Debug(gvar)                    # dependent class       
            self.api = Api(gvar, coin)                  # dependent class
            self.csv = Csv(gvar, coin)                  # dependent class         
            self.trade = Trade(gvar, coin)              # dependent class                 
             
            self.folder_gmo =  f'{self.gvar.domain}/{self.gvar.exchange}/'  # desktop-pc/bot/            
            self.folder_trading_type =  f'{self.gvar.domain}/{self.gvar.exchange}/{self.coin.trade_type}/'  # desktop-pc/bot/buy_sell/             
    
            self.debug.print_log(f"{Bcolors.OKGREEN}{self.gvar.exchange.upper()} BuySell({self.gvar.althorithm_id},{self.gvar.buysell_condition}){Bcolors.ENDC} class initiated: {self.coin.symbol}, qty=[{self.coin.qty:,.{self.coin.qty_decimal}f}], rate=[{self.coin.profit_rate:.4f}, {self.coin.stop_loss_rate:.4f}], debug1={self.coin.debug_mode_debug1}, debug2={self.coin.debug_mode_debug2} ")  
            return     
        
        #########################
        def __str__(self) -> str:
            return f"BuySell({self.folder_gmo=}, {self.folder_trading_type=}, {self.gvar.real_mode=}, {self.gvar.reset_mode=}, {self.gvar.althorithm_id=}, {self.coin.symbol=})"  
    
        ######################################## 
        def reset_restart_process(self) -> None:       
            """
            
            """
            self.gvar.callers_method_name = 'reset_restart_process():'
            symbol = self.coin.symbol
    
            # get master df from the coin class
            master_df = self.coin.master_df             
            if master_df.shape[0] == 0: 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - master_df is empty {master_df.shape[0]=} return ")
                self.debug.sound_alert(3)    
                return                         
    
            order_qty = self.coin.qty          
            mas_df = master_df                                    
    
            # ---------------------------------------------------------
            #  #   Column     Dtype
            # ---------------------------------------------------------
            #  0   price      float64
            #  1   side       object
            #  2   size       float64
            #  3   timestamp  datetime64[ns, UTC]    
            # --------------------------------------------------------                  
    
            ### get latest buy order info from the GMO dataframe (timestamp is sorted by ascending order)
    
            buy_time = mas_df.iloc[0]['timestamp']      # get oldest time
            buy_price = mas_df.iloc[0]['price']         # get oldest price
            buy_order_id = 'FakeB:' + dt.datetime.now().strftime('%m%d-%H%M%S-%f')       # FakeB:MMDD-HHMMSS-999999(milliseconds)
            buy_position_id = 'PosiB:' + dt.datetime.now().strftime('%m%d-%H%M%S-%f')    # PosiB:MMDD-HHMMSS-999999(milliseconds)            
    
            # round time
            freq = '1T'                           
            buy_time = buy_time.round(freq=freq)                                        # round x minutes (where x is 1,4,10,15,30)   
            buy_time = buy_time.round(freq='S')                                         # round seconds
            buy_time = buy_time - timedelta(hours=1)                                    # substract 1 hour
    
            ### get sell order info from the GMO dataframe
               
            sell_time = buy_time # + timedelta(seconds=5)
            sell_price = self.trade.get_sell_price(buy_price)                                  
            sell_order_id = 'FakeS:' + dt.datetime.now().strftime('%m%d-%H%M%S-%f')      # FakeS:MMDD-HHMMSS-999999(milliseconds)
            sell_position_id = 'PosiS:' + dt.datetime.now().strftime('%m%d-%H%M%S-%f')   # PosiS:MMDD-HHMMSS-999999(milliseconds)     
    
            # reset mode ?
            if self.gvar.reset_mode or self.coin.master_csv_not_found:      
                if self.gvar.reset_mode:
                    self.debug.print_log(f".... {self.gvar.callers_method_name} processing({symbol} reset): delete_allfiles() ▶ add a fake data into the buy/sell order csv files...")
                else:    
                    self.debug.print_log(f".... {self.gvar.callers_method_name} processing({symbol} restart): master_csv_not_found(True) ▶ add a fake data into the buy/sell order csv files...")    
                
                ### 1) add a fake data into the buy order, buy order sub files
    
                # write_buy_order(buy_time: utcnow, sell_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:
                self.csv.write_buy_order(buy_time, sell_time, order_qty, buy_price, order_qty, buy_price, 0.0)                                              
    
                # write_buy_order_sub(buy_time: utcnow, sell_time: utcnow, buy_order_id: str, sell_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None: 
                self.csv.write_buy_order_sub(buy_time, sell_time, buy_order_id, sell_order_id, buy_position_id, order_qty, buy_price, order_qty, buy_price, 0.0)   
    
                ### 2) add a fake data into the sell order, sell sub_order log file
    
                # write_sell_order(sell_time: utcnow, buy_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:     
                self.csv.write_sell_order(sell_time, buy_time, order_qty, sell_price, order_qty, sell_price, 0.0)                     
                
                # write_sell_order_sub(sell_time: utcnow, buy_time: utcnow, sell_order_id: str, buy_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:   
                self.csv.write_sell_order_sub(sell_time, buy_time, sell_order_id, buy_order_id, sell_position_id, order_qty, sell_price, order_qty, sell_price, 0.0)
             
            else: # restart mode
                self.debug.print_log(f".... {self.gvar.callers_method_name} processing({symbol} restart): skip close pending orders for restart...")     
            # end of if self.gvar.reset_mode: 
    
            return        
    
        ############################################## 
        def buy_sell(self, algorithm_id: int) -> None:  
            """
            
            """
            if algorithm_id <= 3:
                eval(f'self.buy_sell_algorithm_{str(algorithm_id)}()')    # call 'self.buy_sell_algorithm_?() method
            else:
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}Invalid Algorithm_id {algorithm_id=} {Bcolors.ENDC} -  skip buy_sell ")
                self.debug.sound_alert(3)   
    
            # if algorithm_id == 0:
            #     self.buy_sell_algorithm_0() 
            # elif algorithm_id == 1:
            #     self.buy_sell_algorithm_1()                
            # elif algorithm_id == 2:             
            #     self.buy_sell_algorithm_2()
            # elif algorithm_id == 3:             
            #     self.buy_sell_algorithm_3()            
            # else:
            #     self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}Invalid Algorithm_id {algorithm_id=} {Bcolors.ENDC} -  skip buy_sell ")
            #     self.debug.sound_alert(3)    
    
            return
    
        #######################################
        def buy_sell_algorithm_0(self) -> None:
            """
     
            """
            self.gvar.callers_method_name = 'buy_sell(0):'  
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal               
            df = self.coin.master2_df
            mas_df = pd.DataFrame(df) # cast dataframe()
    
            ### filter buy master
            filter_mask = mas_df['side'] == 'BUY'
            mas_buy_df = mas_df[filter_mask]
            # mas_buy_df is less than trend_search_count ? => SYTEM ERROR
            if mas_buy_df.shape[0] < self.coin.trend_search_count:  # < 3 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - mas_buy_df({mas_buy_df.shape[0]}) is less than {self.coin.trend_search_count}. skip buy_sell() ")
                self.debug.sound_alert(3)    
                return   
    
            ### filter sell master
            filter_mask = mas_df['side'] == 'SELL'
            mas_sell_df = mas_df[filter_mask]
            # mas_sell_df is less than trend_search_count ? => SYTEM ERROR
            if mas_sell_df.shape[0] < self.coin.trend_search_count:  # < 3 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - mas_sell_df({mas_sell_df.shape[0]}) is less than {self.coin.trend_search_count}. skip buy_sell() ")
                self.debug.sound_alert(3)    
                return 
         
            self.debug.trace_write(f".... {self.gvar.callers_method_name} buy_sell_condition({self.gvar.buysell_condition}), mas_buy_df.shape({mas_buy_df.shape[0]}),  mas_sell_df.shape({mas_sell_df.shape[0]}) ")  
    
            # mas_df:
            # -------------------------------------------------------------
            #  #   Column      Dtype
            # -------------------------------------------------------------
            #  0   time        datetime64[ns, UTC]
            #  1   open        float64
            #  2   close       float64
            #  3   low         float64
            #  4   high        float64
            #  5   price       float64
            #  6   buy_price   float64
            #  7   sell_price  float64
            #  8   side        object
            #  9   size        float64
            #  10  symbol      object
            # ---------------------------------------------------------------
                           
            position = 'buy or sell'                                     
    
            ### load buy order / buy order sub files
            buy_df = self.csv.get_buy_order()             # ★   
            buy_sub_df = self.csv.get_buy_order_sub()     # ★         
    
            ### load sell order / sell order sub files
            sell_df = self.csv.get_sell_order()           # ★ 
            sell_sub_df = self.csv.get_sell_order_sub()   # ★  
      
            ### update the position depending on buy/sell sub order log files            
    
            ### get the last buy sub order info
            last_buy_time  = buy_sub_df.iloc[-1]['buy_time']  
            last_sell_time = buy_sub_df.iloc[-1]['sell_time']             
            last_buy_real_qty = buy_sub_df.iloc[-1]['real_qty']
            last_buy_real_price = buy_sub_df.iloc[-1]['real_price']        
       
            ### get the last sell order (pair of the buy order)
    
            # find the pair of the sell order
            find_mask = sell_df['sell_time'] == last_sell_time
            dfx = sell_df[find_mask]
            # not found sell order ?
            if dfx.shape[0] == 0:
                # self.debug.print_log(f".... {bcolors.WARNING}{self.gvar.callers_method_name}{bcolors.ENDC} ▶ sell order not found: {last_sell_time=:%Y-%m-%d %H:%M:%S} ")
                last_sell_real_qty = 0
            else: ### found pair of the sell order
                # get sell order real_qty
                ix = dfx.index[0]
                last_sell_real_qty = dfx.loc[ix, 'real_qty']
            
            # closed order ?
            if round(last_buy_real_qty, _q_) == round(last_sell_real_qty, _q_):
                position = 'buy'    # sell order not required => position(BUY) 
                self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}NEW BUY ORDER REQUEST{Bcolors.ENDC} ")     
            else: # not found sell order or incomplete sell order
                return  # skip return
            # end of if round(last_buy_real_qty, digits) == round(last_sell_real_qty, digits):  
    
            # buy position ? 
            if position.startswith('buy'):       
                self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}BUY{Bcolors.ENDC} position (long position) ")
                
                ### BUY position only
    
                ### check buy condition and create a buy order
        
                # get the latest time & buy_price from GMO master dataframe
                mas_time  = mas_buy_df.iloc[-1]['time']     # gmo time      ★             
                mas_price = mas_buy_df.iloc[-1]['price']    # gmo price    ★                
    
                while True:    
    
                    ###########################################################
                    ### 1: check duplicate buy order
                    ###########################################################
    
                    # mas_time(gmo) is less than or equal to last_buy_time ? => SKIP
                    if mas_time <= last_buy_time: 
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP(1){Bcolors.ENDC} mas_time({mas_time:%Y-%m-%d %H:%M:%S}) is less than or equal to last_buy_time({last_buy_time:%Y-%m-%d %H:%M:%S}) ")      
                        break   # exit while true loop              
                                                        
                    # duplicate buy order ? => SKIP
                    # find_order(df: pd.DataFrame, col: str, val=any) -> bool:
                    if self.trade.find_order(buy_df, col='buy_time', val=mas_time):   
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP(2){Bcolors.ENDC} duplicate buy order: buy_time={mas_time:%Y-%m-%d %H:%M:%S}) ") 
                        break   # exit while true loop 
    
                    ### no duplicate buy order      
    
                    ###########################################################
                    ### 2: execute buy order (MARKET) & sell order (LIMIT)
                    ###########################################################                
    
                    qty = self.coin.qty               
                           
                    ### execute buy order with qty (MARKET) 
                    # create_buy_order(buy_time: utcnow, qty: float, price: float) -> int:  # return status  
                    status = self.create_buy_order(mas_time, qty, mas_price)    # ★ price is used for fake mode: execute buy order with market price
    
                    if status == 0:
                        # reload buy order sub file     
                        buy_sub_df = self.csv.get_buy_order_sub()     
    
                        # get the last buy order sub info
                        last_buy_time  = buy_sub_df.iloc[-1]['buy_time']        
    
                        # get the latest sell price from gmo master
                        ms_price = mas_sell_df.iloc[-1]['price']                                   
    
                        ### execute sell order (LIMIT) 
                        # create_sell_order(sell_time: utcnow, ms_price: float, buy_time: utcnow) -> int:               
                        status = self.create_sell_order(mas_time, mas_price, last_buy_time) # ★ 
    
                    break   # exit while true loop            
                # end of while True:                
            
            return           
    
        ####################################### 
        def buy_sell_algorithm_1(self) -> None:     
            """
    
            """
            self.gvar.callers_method_name = 'buy_sell(1):' 
            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}buy_sell_algorithm_2() has not been implemented yet{Bcolors.ENDC} -  skip buy_sell(2) ")
            self.debug.sound_alert(3)
     
            return            
    
        ####################################### 
        def buy_sell_algorithm_2(self) -> None:     
            """
            
            """
            self.gvar.callers_method_name = 'buy_sell(2):' 
            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}buy_sell_algorithm_2() has not been implemented yet{Bcolors.ENDC} -  skip buy_sell(2) ")
            self.debug.sound_alert(3)
            return 
    
        #######################################
        def buy_sell_algorithm_3(self) -> None:     
            """
            
            """
            self.gvar.callers_method_name = 'buy_sell(3):' 
            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}buy_sell_algorithm_3() has not been implemented yet{Bcolors.ENDC} -  skip buy_sell(3) ")
            self.debug.sound_alert(3)
            return                 
    
        ########################################################################################## 
        def create_buy_order(self, buy_time: dt.datetime.utcnow, qty: float, price: float) -> int:  # return status : price (latest buy market price)        
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal        
            
            self.gvar.callers_method_name = 'create_buy_order():'
            self.debug.trace_write(f".... {self.gvar.callers_method_name} create_buy_order({buy_time=:%Y-%m-%d %H:%M:%S}, {qty=:,.{_q_}f}, {price=:,.{_p_}f}) ")  
            status = 0
    
            # reset buy/sell position count
            self.coin.position_count_0_10 = 0        
            self.coin.position_count_11_30 = 0       
            self.coin.position_count_31_50 = 0      
            self.coin.position_count_51_70 = 0      
            self.coin.position_count_71_90 = 0      
            self.coin.position_count_91_999 = 0     
        
            ################################################
            ### 【1】CHECK DUPLICATE BUY ORDER 
            ################################################
    
            ### check duplicate buy order 
    
            # OPEN (load the buy order file) : buy_order(BTC_JPY).csv 
            buy_df = self.csv.get_buy_order()             
    
            # find a buy order 
            find_mask = buy_df['buy_time'] == buy_time
            dfx = buy_df[find_mask]          
            # duplicate buy order ?     
            if dfx.shape[0] > 0:
                status = 9      # set internal status code 
                self.debug.trace_write(f".... {self.gvar.callers_method_name} duplicate buy order ({buy_time=:%Y-%m-%d %H:%M:%S}) ▶ {status=} error return... ")
                return status   # error return       
    
            # reset sell_position_list
            self.coin.sell_position_list = [] 
    
            #############################################################
            ### 【2】EXECUTE BUY ORDER MARKET (FAK: Fill And Kill)
            #############################################################
    
            ### execute buy order with market price : FAK (Fill And Kill)       
    
            buy_qty = qty       # save original buy qty  ★
    
            # define buy order header list
            header_real_qty_list = []
            header_real_price_list = []
            header_real_fee_list = []          
    
            status = 0
            loop_count = 0      
    
            while True:
                loop_count += 1
    
                # exe_buy_order_market_wait(qty: float, price: float, latest_close_price=0.0) -> tuple: 
                status, res_df, msg_code, msg_str = self.api.exe_buy_order_market_wait(buy_qty, price)  # ★ FAK (Fill And Kill) : price is used for fake mode  
                res_df = pd.DataFrame(res_df)   # cast           
                self.debug.print_log(f".... {self.gvar.callers_method_name} while({loop_count}): exe_buy_order_market_wait() ▶ {status=}, {res_df.shape=} ")
    
                ####################################################################### DEBUG(1_1) DEBUG(1_1) DEBUG(1_1) : create_buy_order()           
                if not self.coin.real_mode:
                    if self.coin.debug_mode_debug1: 
                        # ★ create two buy order subs for each buy order header ★
                        # update_get_price_response_for_debug1_1(res_df: pd.DataFrame, loop_count: int) -> pd.DataFrame:
                        res_df = self.trade.update_get_price_response_for_debug1_1(res_df, loop_count)   
                        # loop_count == 1: return 2 rows => add 2 buy order subs
                        # loop_count == 2: return 1 row  => add 1 buy order sub                     
                # end of if not self.coin.real_mode:
                ###################################################################### DEBUG(1_1) DEBUG(1_1) DEBUG(1_1) : create_buy_order()
    
                # exe_buy_order_market_wait() error ?
                if status != 0:
                    if status == 1 and msg_code == 'ERR-201': # Trading margin is insufficient ?      
                        self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_buy_order{Bcolors.ENDC}({buy_qty=:,.{_q_}f}, {price=:,.{_p_}f}): ▶ {Bcolors.FAIL}the trading margin is insufficient error: ERR-201{Bcolors.ENDC} ")
                        self.debug.sound_alert(2)  
                        break       # exit while true loop => error return         
                    elif status == 8 and msg_code == 'ERR-888': # get_price() time out error ?
                        self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_buy_order{Bcolors.ENDC}({buy_qty=:,.{_q_}f}, {price=:,.{_p_}f})): ▶ {Bcolors.FAIL}get_price() time out error: {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                        break       # exit while true loop => error return 
                    else: # misc error
                        self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_buy_order{Bcolors.ENDC}({buy_qty=:,.{_q_}f}, {price=:,.{_p_}f})): ▶ {Bcolors.FAIL}misc error: {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                        break       # exit while true loop => error return 
                # end of if status != 0:
                             
                ### no error : res_df.shape[0] >= 1 (single row or multiple rows)            
                       
                ################################################################################################################
                ### 【3】ADD A BUY ORDER CHILD
                ################################################################################################################
    
                # res_df = get_price() response for exe_buy_order_market_wait()
                # ---------------------------------------------------------
                #  #   Column       Dtype              
                # ---------------------------------------------------------              
                #  0   executionId  object             
                #  1   fee          float64            
                #  2   lossGain     float64            
                #  3   orderId      object             
                #  4   positionId   object  ★           
                #  5   price        float64            
                #  6   settleType   object  (OPEN or CLOSE)             
                #  7   side         object  (BUY or SELL)           
                #  8   size         float64            
                #  9   symbol       object  (BTC_JPY)           
                #  10  timestamp    datetime64[ns, UTC]
                # --------------------------------------------------------- 
    
                ### write a buy order child
          
                # define buy order child list
                buy_order_id_list = []
                position_id_list = []
                real_qty_list = []
                real_price_list = []
                real_fee_list = []   
    
                # response df for exe_buy_order_market_wait(FAK) 
                for k in range(len(res_df)):
                    buy_order_id    = res_df.loc[k, 'orderId']          # buy order id (same order id)
                    position_id     = res_df.loc[k, 'positionId']       # position id (unique id)
                    real_qty        = res_df.loc[k, 'size']             # real qty   ★ 
                    real_price      = res_df.loc[k, 'price']            # real price ★
                    real_fee        = res_df.loc[k, 'fee']              # real fee
    
                    buy_order_id_list.append(buy_order_id)              # B9999-999999-999999
                    position_id_list.append(position_id)                # P9999-999999-999999
                    real_qty_list.append(real_qty)                      # 10, 10, 20 = 40
                    real_price_list.append(real_price)                  # XRP_JPY(64.470)
                    real_fee_list.append(real_fee)                      # zero(0)
    
                    # write_buy_order_child(buy_time: utcnow, buy_order_id: str, sell_time: utcnow, sell_order_id: str, position_id: str , qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:        
                    _sell_time_ = buy_time; _sell_order_id_ = 'Dummy';  _qty_ = real_qty; _price_ = real_price  
                    self.csv.write_buy_order_child(buy_time, buy_order_id, _sell_time_, _sell_order_id_, position_id, _qty_, _price_, real_qty, real_price, real_fee)
                # end of for k in range(len(res_df)): 
    
                first_buy_order_id = buy_order_id_list[0]   # same order ids              
                position_ids_by_semicolon = ''  # 'positionId1;positionId2;positionId3'
                for k, position_id_x in enumerate(position_id_list):
                    if k == 0:
                        position_ids_by_semicolon += position_id_x
                    else:
                        position_ids_by_semicolon += ';' + position_id_x    
    
                sub_sum_real_qty = round(sum(real_qty_list), _q_)   # XRP_JP(9999)               
                sub_sum_real_fee = round(sum(real_fee_list), 0)     # zero(0)
    
                if sum(real_price_list) > 0:
                    sub_mean_real_price = round(statistics.mean(real_price_list), _p_)  # XRP_JPY(99.123)
                else:
                    sub_mean_real_price = 0                      
    
                header_real_qty_list.append(sub_sum_real_qty)        
                header_real_price_list.append(sub_mean_real_price)
                header_real_fee_list.append(sub_sum_real_fee)                   
    
                ###################################################################################################################
                ### 【4】ADD A BUY ORDER SUB (buy_time, sell_time,...) ★ sell_time <= buy_time
                ###################################################################################################################
    
                ### write a buy order sub 
                # write_buy_order_sub(buy_time: utcnow, sell_time: utcnow, buy_order_id: str, sell_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:
                _sell_time_ = buy_time; _sell_order_id_ = 'Dummy';  _qty_ = sub_sum_real_qty # positionId = 'positionId1;positionId2;positionId3'
                self.csv.write_buy_order_sub(buy_time, _sell_time_, first_buy_order_id, _sell_order_id_, position_ids_by_semicolon, _qty_, price, sub_sum_real_qty, sub_mean_real_price, sub_sum_real_fee) # ★
    
                ### check exit condition     
    
                header_sum_real_qty = round(sum(header_real_qty_list), _q_)     # XRP_JPY(9999)           
                # self.debug.trace_write(f".... {self.gvar.callers_method_name} while({loop_count}): buy_qty={buy_qty}, header_sum_real_qty={header_sum_real_qty} ")  
                
                ############################## TEMP TEMP TEMP
                # temp_qty = round(qty, _q_) 
                # temp_real_qty = round(header_sum_real_qty, _q_)
                # self.debug.trace_warn(f".... DEBUG: {temp_qty=:,.{_q_}f}, {temp_real_qty=:,.{_q_}f} ")
                ############################# TEMP TEMP TEMP
               
                # no outstanding buy_qty ?
                if round(qty, _q_) == round(header_sum_real_qty, _q_):  
                    # increment buy count      
                    self.gvar.buy_count += 1  
                    self.coin.buy_count += 1                  
                    break   # exit while true loop             
                else:       # outstanding buy_qty exists      
                    pass    # skip (do nothing)                      
                # end of if round(buy_qty, _q_) == round(header_sum_real_qty, _q_):  
    
                ### partial exe_buy_order_market_wait() was executed => continue to buy remaining qty
                    
                remaining_qty = buy_qty - header_sum_real_qty   # calculate new buy qty        
    
                buy_qty = remaining_qty # update new buy qty      
    
                # continue exe_buy_order_market_wait() ★           
            # end of while True:    
    
            #################################################################################
            ### 【5】ADD A BUY ORDER HEADER (buy_time, sell_time) ★ sell_time <= buy_time
            #################################################################################
    
            ### update buy order header(real_qty, real_price, real_fee)
            header_sum_real_qty = round(sum(header_real_qty_list), _q_)     # XRP_JPY(9999)    
            header_sum_real_fee = round(sum(header_real_fee_list), 0)       # zero(0)        
    
            if sum(header_real_price_list) > 0:
                header_mean_real_price = round(statistics.mean(header_real_price_list), _p_)  # XRP_JPY(99.123)
            else:
                header_mean_real_price = 0            
    
            ### write a buy order  
            # write_buy_order(buy_time: utcnow, sell_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:       
            _sell_time_ = buy_time  
            self.csv.write_buy_order(buy_time, _sell_time_, qty, price, header_sum_real_qty, header_mean_real_price, header_sum_real_fee)       
             
            self.debug.trace_write(f".... {self.gvar.callers_method_name} create_buy_order() ▶ {status=} normal return... ")                
    
            return status          
    
        #################################################################################################################  
        def create_sell_order(self, sell_time: dt.datetime.utcnow, ms_price: float, buy_time: dt.datetime.utcnow) -> int:   # return status : ms_price (latest market sell price)   
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal                
    
            self.gvar.callers_method_name = 'create_sell_order():'
            self.debug.trace_write(f".... {self.gvar.callers_method_name} create_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, {ms_price=:,.{_p_}f}, {buy_time=:%Y-%m-%d %H:%M:%S}) ") 
    
            status = 0    
            
            ################################################
            ### 【1】CHECK DUPLICATE SELL ORDER 
            ################################################
    
            ### check duplicate sell order 
    
            # get the sell order : sell_order(BTC_JPY).csv
            sell_df = self.csv.get_sell_order()          
    
            # find a sell order 
            find_mask = sell_df['sell_time'] == sell_time   # PK(sell_time)
            dfx = sell_df[find_mask]          
            # duplicate sell order ?     
            if dfx.shape[0] > 0:
                status = 9      # set internal status code
                self.debug.trace_write(f".... {self.gvar.callers_method_name} duplicate sell order ({sell_time=:%Y-%m-%d %H:%M:%S}) ▶ {status=} error return... ")
                return status   # error return 
              
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv          
            buy_sub_df = self.csv.get_buy_order_sub()  
    
            ### OPEN (load the buy order child csv file ) : buy_order_child(BTC_JPY).csv          
            buy_ch_df = self.csv.get_buy_order_child()  
       
            ######################################################################
            ### 【2】READ ALL BUY ORDER SUBs (filter by buy_time)
            ######################################################################        
            
            # define sell order sub list
            sub_qty_list = []
            sub_price_list = []
    
            # filter the buy order sub by buy_time
            filter_mask = buy_sub_df['buy_time'] == buy_time
            buy_subx_df = buy_sub_df[filter_mask]
            if buy_subx_df.shape[0] > 0:
                buy_subx_df.reset_index(inplace=True)
    
            # get all buy order sub filtered by buy_time 
            for i in range(len(buy_subx_df)):   # ★ buy_subx_df ★
                ### get buy order sub info
                sub_buy_time        = buy_subx_df.loc[i, 'buy_time']        # non unique                 
                sub_buy_order_id    = buy_subx_df.loc[i, 'buy_order_id']    # unique id  
                sub_qty             = buy_subx_df.loc[i, 'qty']                                    
                sub_price           = buy_subx_df.loc[i, 'price']               
                sub_real_qty        = buy_subx_df.loc[i, 'real_qty']                            
                sub_real_price      = buy_subx_df.loc[i, 'real_price']            
                sub_real_fee        = buy_subx_df.loc[i, 'real_fee']                  
                      
                #################################################################################
                ### 【3】READ ALL BUY ORDER CHILD (filter by buy_time + buy_order_id)
                #################################################################################  
    
                # define sell order child list
                ch_buy_order_id_list = []
                ch_sell_order_id_list = []
                ch_position_id_list = []
                ch_qty_list = []
                ch_price_list = []                  
    
                ### filter the buy order child by buy_time + buy_order_id
                
                # buy order child is empty ?
                if buy_ch_df.shape[0] == 0:            
                    buy_chx_df = pd.DataFrame()
                else: # filter buy order child    
                    filter_mask = (buy_ch_df['buy_time'] == buy_time) & (buy_ch_df['buy_order_id'] == sub_buy_order_id)
                    buy_chx_df = buy_ch_df[filter_mask]
                    if buy_chx_df.shape[0] > 0:
                        buy_chx_df.reset_index(inplace=True)
                # end of if buy_ch_df.shape[0] == 0: 
    
                # get all buy order child filtered by buy_time + buy_order_id
                for j in range(len(buy_chx_df)):   # ★ buy_chx_df ★
                    ### get buy order child info
                    ch_buy_time     = buy_chx_df.loc[j, 'buy_time']     # non unique time       
                    ch_buy_order_id = buy_chx_df.loc[j, 'buy_order_id'] # non unique id (same buy_order_id)      
                    ch_position_id  = buy_chx_df.loc[j, 'position_id']  # unique id
                    ch_qty          = buy_chx_df.loc[j, 'qty']                                    
                    ch_price        = buy_chx_df.loc[j, 'price']               
                    ch_real_qty     = buy_chx_df.loc[j, 'real_qty']                            
                    ch_real_price   = buy_chx_df.loc[j, 'real_price']            
                    ch_real_fee     = buy_chx_df.loc[j, 'real_fee'] 
    
                    #############################################################################################
                    ### 【4】EXECUTE SELL CLOSE ORDER LIMIT (FAS:Fill And Store) FOR EACH BUY ORDER CHILD 
                    ############################################################################################# 
           
                    sell_qty = ch_real_qty
                    sell_price = self.trade.get_sell_price(ch_real_price, trace=True)                          
    
                    # market price is greater than sell price
                    if ms_price > sell_price:
                        sell_price = ms_price                  
                  
                    ### execute a sell close order with limit(FAS) 
                    # exe_sell_close_order_limit(position_id: str, qty: float, price: float) -> float:
                    status, res_sell_order_id, msg_code, msg_str = self.api.exe_sell_close_order_limit(ch_position_id, sell_qty, sell_price) # ★      
                    # res_sell_order_id => 'S9999-999999-999999'       
    
                    # exe_sell_close_order_limit() error ?
                    if status != 0:
                        if status == 1 and msg_code == 'ERR-208':   # Exceeds the available balance
                            self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_sell_order{Bcolors.ENDC}({sell_qty=:,.{_q_}f}): ▶ {Bcolors.FAIL}the available balance exceeded ERR-208: continue...{Bcolors.ENDC} ")
                            self.debug.sound_alert(2)  
                            continue    # continue to next buy order child        
                        else: # misc error
                            self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_sell_order{Bcolors.ENDC}({sell_qty=:,.{_q_}f}): ▶ {Bcolors.FAIL}misc error: {status=}, {msg_code} - {msg_str} {Bcolors.ENDC} ")                               
                            self.debug.sound_alert(2) 
                            continue    # continue to next buy order child             
                    # end of if status != 0:
    
                    ### no error for exe_sell_close_order_limit()        
    
                    ################################################################################################################
                    ### 【5】ADD A SELL ORDER SUB
                    ################################################################################################################
                            
                    ### write a sell order sub
                    # write_sell_order_sub(sell_time: utcnow, buy_time: utcnow, sell_order_id: str, buy_order_id: str, position_id: str, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:                                                         
                    self.csv.write_sell_order_sub(sell_time, buy_time, res_sell_order_id, ch_buy_order_id, ch_position_id, sell_qty, sell_price)       
     
                    ch_buy_order_id_list.append(ch_buy_order_id)
                    ch_sell_order_id_list.append(res_sell_order_id)
                    ch_position_id_list.append(ch_position_id)
                    ch_qty_list.append(sell_qty)
                    ch_price_list.append(sell_price)
    
                    # continue to next buy order child
                # end of for j in range(len(buy_chx_df)):    
    
                ch_sum_qty = round(sum(ch_qty_list), _q_)                               
                if sum(ch_price_list) > 0:
                    ch_mean_price = round(statistics.mean(ch_price_list), _p_)  
                else:
                    ch_mean_price = 0    
    
                sub_qty_list.append(ch_sum_qty)
                sub_price_list.append(ch_mean_price)                             
    
                #####################################################################################
                ### 【6】UPDATE BUY ORDER SUB (sell_order_id, sell_time)
                #####################################################################################          
    
                sell_order_ids_by_semicolon = ''    # 'SellOrderID1;SellOrderID2;SellOrderID3'
                for k, sell_order_id_x in enumerate(ch_sell_order_id_list):
                    if k == 0:
                        sell_order_ids_by_semicolon += sell_order_id_x
                    else:
                        sell_order_ids_by_semicolon += ';' + sell_order_id_x    
    
                ### update buy order sub (sell_order_id, sell_time) 
                find_mask = (buy_sub_df['buy_time'] == buy_time) & (buy_sub_df['buy_order_id'] == sub_buy_order_id)
                dfx = buy_sub_df[find_mask]
                # not found buy order sub ?
                if dfx.shape[0] == 0:
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}create_sell_order{Bcolors.ENDC}(): {Bcolors.FAIL} buy order sub not found ▶ {sub_buy_order_id=} {Bcolors.ENDC} ")
                else: # found buy order sub => update sell_order_id, sell_time 
                    buy_sub_df.loc[find_mask, 'sell_order_id'] = sell_order_ids_by_semicolon # 'SellOrderID1;SellOrderID2;SellOrderID3'  
                    buy_sub_df.loc[find_mask, 'sell_time'] = sell_time
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} update_buy_order_sub({buy_time=:%Y-%m-%d %H:%M:%S}, {sub_buy_order_id=}, {sell_order_ids_by_semicolon=}, {sell_time=:%Y-%m-%d %H:%M:%S}) ")  
                # end of if dfx.shape[0] == 0:  
    
                # continue to next buy order sub           
            # end of for i in range(len(buy_subx_df)):          
    
            #################################################################################
            ### 【7】SAVE BUY ORDER SUB TO CSV FILE
            #################################################################################
    
            ### CLOSE (save the buy order sub csv file) : buy_order_sub(BTC_JPY).csv     
            csv_file = self.folder_trading_type + f'buy_order_sub({symbol}).csv' 
            # desktop-pc/bot/buy_sell/buy_order_sub(BTC_JPY).csv  
            buy_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode                                     
              
            ################################################################################
            ### 【8】ADD A SELL ORDER 
            ################################################################################            
    
            ### update sell order (real_qty, real_price, real_fee) ★
            sub_sum_qty = round(sum(sub_qty_list), _q_)                                            
            
            if sum(sub_price_list) > 0:
                sub_mean_price = round(statistics.mean(sub_price_list), _p_)  
            else:
                sub_mean_price = 0    
    
            # write_sell_order(sell_time: utcnow, buy_time: utcnow, qty: float, price: float, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:    
            self.csv.write_sell_order(sell_time, buy_time, sub_sum_qty, sub_mean_price)  
    
            ################################################################################
            ### 【9】UPDATE A BUY ORDER (sell_time)
            ################################################################################          
            # update_buy_order(buy_time: utcnow, col_name_list: list, col_value_list: list) -> None:    # PK(buy_time)
            self.csv.update_buy_order(buy_time, col_name_list=['sell_time'], col_value_list=[sell_time])    
    
            self.debug.trace_write(f".... {self.gvar.callers_method_name} create_sell_order() ▶ {status=} normal return... ")
    
            return status            
        
        ###############################
        def update_order(self) -> None:
            """
            
            """
            ########################################################################
            ### 【1】UPDATE CLOSED SELL ORDER 
            ########################################################################
            ### update closed sell order
            self.update_closed_order()   
    
            ########################################################################
            ### 【2】UPDATE ORDER PROFIT (PART) : first_pass(True)
            ########################################################################
            ### update order profit 
            self.update_order_profit(first_pass=True)           
    
            ########################################################################
            ### 【3】CHECK STOP LOSS : gvar.stoploss_method_id : 0, 1,...
            ########################################################################
            ### check latest price(GMO) and stop loss price 
            if self.gvar.stop_loss:    
                self.stop_loss(self.gvar.stoploss_method_id)    # 0, 1,...  
           
            ########################################################################
            ### 【4】CANCEL PENDING ORDERS OR CLOSE PENDING ORDERS 
            ########################################################################
            ### cancel pending orders
            # cancel pending order ? => call cancel_pending_orders() method       
            if self.gvar.cancel_pending_orders:         
                self.cancel_pending_order()            
            elif self.gvar.close_pending_orders: # close pending orders (current time is between 05:00AM - 06:00AM)  
                self.close_pending_order()                          
    
            ########################################################################
            ### 【5】 UPDATE ORDER PROFIT (PART2) : first_pass(False)
            ########################################################################
            ### update the order file ( order(BTC).csv )
            if self.gvar.stop_loss or self.gvar.cancel_pending_orders or self.gvar.close_pending_orders:
                self.update_order_profit(first_pass=False)  
    
            # if self.gvar.stop_loss:
            #     self.update_order_profit(first_pass=False)  
            # elif self.coin.cancel_pending_orders: # cancel pending order by coin (one time request)            
            #     self.update_order_profit(first_pass=False)    
            # elif self.gvar.close_pending_orders: # close pending orders (current time is between 05:00AM - 06:00AM)      
            #     self.update_order_profit(first_pass=False) 
    
            return
    
        ###################################### 
        def update_closed_order(self) -> None:  
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal   
            self.gvar.callers_method_name = 'update_closed_order():'
            order_completed = self.trade.get_order_status(exclude=False)   # include stop_loss, cancelled, closed order info
            
            df = self.coin.master2_df    
            mas_df = pd.DataFrame(df)   # cast df (master dataframe)
            self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}last order completed({order_completed}){Bcolors.ENDC}, mas_df.shape({mas_df.shape[0]}), cancel({self.gvar.cancel_pending_orders}) ")       
    
            # mas_df is empty ? => SYTEM ERROR
            if mas_df.shape[0] == 0: 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - mas_df({mas_df.shape[0]}) is empty. skip update_order() ")
                self.debug.sound_alert(3)                  
                return 
    
            ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)       
    
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
    
            # get_orderbooks_try() -> tuple:  # return (sell_df, buy_df)   
            sell_bk, buy_bk = self.api.get_orderbooks_try()
            # sleep(0.5)
            sell_bk_df = pd.DataFrame(sell_bk)  # ascending order of price [-1, price]      : low to high price 100, 110, 120, 130, 140, 150
            buy_bk_df = pd.DataFrame(buy_bk)    # descending order of price iloc[0, price]  : high to low price 150, 140, 130, 120, 110, 100
            
            # not empty order books ?
            if sell_bk_df.shape[0] > 0 and buy_bk_df.shape[0] > 0:                
                # get latest sell/buy price from the order books
                bk_sell_price = sell_bk_df.iloc[0]['price'] # get lowest sell price : 100
                bk_buy_price = buy_bk_df.iloc[0]['price']   # get highest buy price : 150
            else:
                bk_sell_price = 0.0
                bk_buy_price = 0.0
    
            # incomplete sell order (pending sell order) ?
            if not order_completed: 
                ### print GMO orderbooks info (sell price, buy price)
    
                # # get_orderbooks_try() -> tuple:  # return (sell_df, buy_df)   
                # sell_bk, buy_bk = self.api.get_orderbooks_try()
                # sleep(0.5)
                # sell_bk_df = pd.DataFrame(sell_bk)  # ascending order of price [-1, price]      : low to high price 100, 110, 120, 130, 140, 150
                # buy_bk_df = pd.DataFrame(buy_bk)    # descending order of price iloc[0, price]  : high to low price 150, 140, 130, 120, 110, 100
                
                # not empty order books ?
                if sell_bk_df.shape[0] > 0 and buy_bk_df.shape[0] > 0:                
                    # get latest sell/buy price from the order books
                    bk_sell_price = sell_bk_df.iloc[0]['price'] # get lowest sell price : 100
                    bk_buy_price = buy_bk_df.iloc[0]['price']   # get highest buy price : 150
    
                    # get the last sell price from the sell order sub 
                    last_sell_price = sell_sub_df.iloc[-1]['price'] # 120
    
                    # get sell position number (buy => sell)
                    filter_mask = sell_bk_df['price'] < last_sell_price # < 120
                    dfx = sell_bk_df[filter_mask]   # 100, 110
                    sell_position = dfx.shape[0]    # 2
    
                    # update sell position count
                    if sell_position >= 91:
                        self.coin.position_count_91_999 += 1
                    elif sell_position >= 71:
                        self.coin.position_count_71_90 += 1
                    elif sell_position >= 51:
                        self.coin.position_count_51_70 += 1
                    elif sell_position >= 31:
                        self.coin.position_count_31_50 += 1
                    elif sell_position >= 11:
                        self.coin.position_count_11_30 += 1
                    else:
                        self.coin.position_count_0_10 += 1                     
    
                    self.coin.sell_position_list.append(sell_position)  # append sell_position : coin.sell_position=[10, 8, 5, 3, 1, 0]       
                    self.debug.trace_write(f".... {self.gvar.callers_method_name}{Bcolors.WARNING} {sell_position=}{Bcolors.ENDC} => close condition [{last_sell_price=:,.{_p_}f} <= {bk_buy_price=:,.{_p_}f}] ")   # 120 <= 150                                  
    
                    reverse_sell_position_list = self.coin.sell_position_list[::-1] # [0, 1, 3, 5, 8, 10]      
                    if len(reverse_sell_position_list) < 21:
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {reverse_sell_position_list=} ")            
                    else:
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {reverse_sell_position_list[0:21]=} ")     
                else: # empty order books
                    self.coin.sell_position_list = []   # reset sell position list
            # end of if not order_completed: 
    
            #################################################
            ### 【1】READ ALL SELL ORDER 
            #################################################
    
            ### get all sell order 
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']   # unique time                  
                buy_time        = sell_df.loc[i, 'buy_time']    # unique time                        
                sell_qty        = sell_df.loc[i, 'qty']         
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']    
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True        
                     
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell order sub
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell order sub ?
                    if dfx.shape[0] > 0:
                        dfx.reset_index(inplace=True)                
                        sell_order_id = str(dfx.loc[0, 'sell_order_id'])    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sell_order_id})... ")                        
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_order_id}) has been closed: sell_order_closed={sell_order_closed} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_time:%Y-%m-%d %H:%M:%S}) has been closed: sell_order_closed={sell_order_closed} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # continue to next sell order     
                # end of if sell_order_closed:    
    
                ### open or incomplete sell order
    
                ##########################################################################################
                ### 【2】READ ALL SELL ORDER SUB & EXECUTE GET_PRICE() FOR EACH SELL ORDER SUB
                ##########################################################################################
    
                ### for sell order sub : use pandas dataframe groupby()     
     
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order : 2 rows
                for j in range(len(sell_subx_df)):   # ★ sell_subx_df ★
                    sell_order_sub_seq = j + 1 # 1, 2, 3,....
    
                    ### get sell order sub info
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # S9999-999999-999999 ★ (unique id) used for get_price() api
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # B9999-999999-999999 ★ (non unique id)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # (unique id)
                    sub_sell_qty        = sell_subx_df.loc[j, 'qty']                # 10
                    sub_sell_price      = sell_subx_df.loc[j, 'price']            
                    sub_sell_real_qty   = sell_subx_df.loc[j, 'real_qty']           # 10
                    sub_sell_real_price = sell_subx_df.loc[j, 'real_price']            
                    sub_sell_real_fee   = sell_subx_df.loc[j, 'real_fee']  
                    sub_sell_stop_loss  = sell_subx_df.loc[j, 'stop_loss'] 
                    sub_sell_cancelled  = sell_subx_df.loc[j, 'cancelled']        
    
                    sub_sell_order_open = False
                    sub_sell_order_closed = False
                    sub_sell_order_incomplete = False
    
                    if sub_sell_real_qty == 0:
                        sub_sell_order_open = True
                    elif sub_sell_qty == sub_sell_real_qty:
                        sub_sell_order_closed = True
                    else:
                        sub_sell_order_incomplete = True                             
    
                    # fake sell sub order ? => SKIP
                    if sub_sell_order_id.startswith('Fake'): 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id})... ") 
                        continue             
                        
                    # cancelled sell sub order ? => SKIP
                    if sub_sell_cancelled:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled ▶ sub_sell_cancelled={sub_sell_cancelled} ")  
                        continue
    
                    # stop loss sell sub order ? => SKIP
                    if sub_sell_stop_loss:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss ▶ sub_sell_stop_loss={sub_sell_stop_loss} ")  
                        continue            
    
                    # closed sell sub order ? => SKIP 
                    if sub_sell_order_closed:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed ▶ sub_sell_order_closed={sub_sell_order_closed} ")  
                        continue     
    
                    ### open or incomplete sell order sub
    
                    ############################################################
                    ### 【3】UPDATE SELL ORDER SUB (get_price_count+1)
                    ############################################################
    
                    ### update sell order sub (get_price_count += 1)  ★         
                    # DO NOT USE: self.csv.update_sell_order_sub_count(sell_time, sell_order_id)        
                    find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                    dfx = sell_sub_df[find_mask]
                    # not found sell order sub ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub_count({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found: {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order sub      
                        sell_sub_df.loc[find_mask, 'get_price_count'] += 1   # increment get_price_count by 1
                        ix = dfx.index[0]
                        sub_sell_get_price_count = sell_sub_df.loc[ix, 'get_price_count']
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub_count({sub_sell_order_id=}, {sub_sell_get_price_count=})  ")   
                    # end of if dfx.shape[0] == 0:                               
    
                    ### get order_status
                    qty = sub_sell_qty
                    price = sub_sell_price
    
                    # get_order_status_try(order_id: str, qty: float, price: float) ->tuple:    # return (status, order_status)                  
                    status, order_status = self.api.get_order_status_try(sub_sell_order_id, qty, price)     # ★ qty, price are used for fake mode only 
                    # status: 0(ok), 4(invalid request), 8(retry error), 9(internal error), ?(misc error) 
    
                    ### status = 0; order_status = 'EXPIRED'    # DEBUG DEBUG DEBUG
    
                    # get_order_status() error ?
                    if status != 0:                       
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}) error ▶ {Bcolors.FAIL} {status=}, {order_status=} {Bcolors.ENDC} ")                               
                        continue    # continue to next sell order sub                       
    
                    ### get_order_status() : status == 0, order_status in 'WAITING,MODIFYING,CANCELLING,ORDERED,EXECUTED,EXPIRED,CANCELED'
    
                    # pending exe_sell_close_order_limit() request ? : ORDERED(指値注文有効中), EXECUTE(全量約定)
                    if order_status in 'WAITING,MODIFYING,CANCELLING,ORDERED':                               
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}get_order_status_try({sub_sell_order_id}, {qty:,.{_q_}f}, {price:,.{_p_}f}){Bcolors.ENDC} ▶ {order_status=} pending exe_sell_close_order_limit() request ")                         
                        continue    # continue to next sell order sub                  
    
                    ############################################# DEBUG DEBUG DEBUG
                    if self.coin.debug_mode_gmo_stop_loss:
                        order_status = 'EXPIRED'        
                    ############################################# DEBUG DEBUG DEBUG
    
                    ### executed exe_sell_close_order_limit() : order_status in 'EXECUTED(全量約定),EXPIRED(全量失効|一部失効),CANCELED'       
    
                    if order_status in 'EXPIRED,CANCELED':                      
                        ### update sell order, sell order sub and order csv file
                        if order_status == 'CANCELED':
                             self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}): {Bcolors.FAIL}the order has been cancelled by GMO. please confirm the reason{Bcolors.ENDC} ")   
                             self.debug.sound_alert(3)
                             continue    # continue to next sell order sub 
    
                        if order_status == 'EXPIRED':     
                            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}): {Bcolors.FAIL}the order has been expired due to loss cut by GMO{Bcolors.ENDC} ")   
                            self.debug.sound_alert(3)
              
                            ###########################################    
                            ### 1: close sell order sub
                            ###########################################                        
    
                            # update a sell order sub (update real_qty, real_price, stop_loss)                                             
                            # DO NOT USE: self.csv.update_sell_order_sub(sell_time, sub_order_id, symbol, col_name_list, col_value_list) 
                            find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                            dfx = sell_sub_df[find_mask]
                            # not found sell order sub ?
                            if dfx.shape[0] == 0:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found: {dfx.shape=}{Bcolors.ENDC} ")   
                            else: # found sell order sub => update sell order sub (real_qty, real_price, real_fee, stop_loss) 
                                gmo_loss_cut_price = 9_999_999.0
                                gmo_loss_cut_fee = 999.0
                                sell_stop_loss = True                            
                                sell_sub_df.loc[find_mask, 'real_qty']   = sub_sell_qty  
                                sell_sub_df.loc[find_mask, 'real_price'] = gmo_loss_cut_price   # gmo loss cut rate 
                                sell_sub_df.loc[find_mask, 'real_fee']   = gmo_loss_cut_fee     # gmo loss cut fee
                                sell_sub_df.loc[find_mask, 'stop_loss'] = sell_stop_loss
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub({sub_sell_order_id=}, real_qty={sub_sell_qty:,.{_q_}f}, {gmo_loss_cut_price=:,.{_p_}f}, {gmo_loss_cut_fee=:,.0f}, {sell_stop_loss=}) ")   
                            # end of if dfx.shape[0] == 0:  
               
                            #######################################
                            ### 2: close sell order    
                            #######################################                        
    
                            # update sell order (real_qty, real_price, real_fee) 
                            find_mask = sell_df['sell_time'] == sell_time
                            dfx = sell_df[find_mask]
                            # not found sell order ?
                            if dfx.shape[0] == 0:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL} ({sell_time=:%Y-%m-%d %H:%M:%S}) not found: {dfx.shape=}{Bcolors.ENDC} ")   
                            else: # found sell order 
                                gmo_loss_cut_price = 9_999_999.0
                                gmo_loss_cut_fee = 999.0                            
                                sell_df.loc[find_mask, 'real_qty']   = sell_qty                 
                                sell_df.loc[find_mask, 'real_price'] = gmo_loss_cut_price   # gmo loss cut rate 
                                sell_df.loc[find_mask, 'real_fee']   = gmo_loss_cut_fee     # gmo loss cut fee 
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={sell_qty:,.{_q_}f}, {gmo_loss_cut_price=:,.{_p_}f}, {gmo_loss_cut_fee=:,.0f}) ")                                                    
                            # end of if dfx.shape[0] == 0:                                                      
          
                            ###########################################
                            ### 3: update order csv (update stop_loss)    
                            ###########################################  
                             
                            ### OPEN (load the order csv csv file) : order(BTC_JPY).csv 
                            ord_df = self.csv.get_order_csv()                 
                            if ord_df.shape[0] > 0:
                                # find the order csv by buy_order_id        
                                find_mask = ord_df['buy_order_id'] == sub_buy_order_id
                                dfx = ord_df[find_mask]
                                if dfx.shape[0] > 0:                                
                                    # update_order_csv_buy(buy_time: utcnow, buy_order_id: str, col_name_list: list, col_value_list: list) -> None: # PK(buy_time + buy_order_id)
                                    self.csv.update_order_csv_buy(sub_buy_time, sub_buy_order_id, col_name_list=['stop_loss'], col_value_list=[True])                            
    
                            continue    # continue to next sell order sub                  
                        # end of if order_status == 'EXPIRED':     
                    # end of if order_status in 'EXPIRED,CANCELED':                
    
    
                    ### get highest buy price from buy orderbooks
                    latest_book_price = bk_buy_price    # bk_buy_price = buy_bk_df.iloc[0]['price']    
    
                    # ### get real qty, sell price, fee from the GMO
                    # ### get the latest close price from the GMO master              
                    # ### mas_latest_close_price = mas_df.iloc[-1]['close']   # get latest close price (GMO)              
    
                    _debug1_ = self.coin.debug_mode_debug1; _debug2_ = self.coin.debug_mode_debug2    
    
                    ######################################################################################### DEBUG(1), DEBUG(2)           
                    # _debug1_ = True; _debug2_ = True    # ★ TEMP TEMP TEMP ★   
                    # self.debug.trace_warn(f".... DEBUG _debug1_ = True; _debug2_ = True ")              
                    ######################################################################################### DEBUG(1), DEBUG(2)  
    
                    ### get sell real qty, price & fee from the GMO : get_price() for exe_sell_close_order_limit() ★     
                    
                    sleep(1)    # sleep 1 sec            
                    # get_price_try(order_id: str, qty: float, price: float, latest_book_price=0.0, debug1=False, debug2=False) -> tuple: 
                    status, res_df, msg_code, msg_str = self.api.get_price_try(sub_sell_order_id, sub_sell_qty, sub_sell_price, latest_book_price, _debug1_, _debug2_)  # ★ qty, price, close_price are used for fake mode                  
                    # status: 0(ok),  8(retry error), 9(empty dataframe or internal error), ?(misc error)
                    sleep(1)    # sleep 1 sec            
    
                    res_df = pd.DataFrame(res_df)   # cast dataframe
    
                    # not real mode and debug (stop loss or cancel pending orders) ?
                    ######################################################################################### DEBUG(1_2) DEBUG(1_2) DEBUG(1_2) : update_order()
                    if not self.coin.real_mode:
                        if self.coin.debug_mode_debug1: 
                            if res_df.shape[0] > 0:
                                # ★ create two sell order subs for each sell order header ★
                                # update_get_price_response_for_debug1_2(res_df: pd.DataFrame, sell_order_sub_seq: int) -> pd.DataFrame
                                res_df = self.trade.update_get_price_response_for_debug1_2(res_df, sell_order_sub_seq)  # 1, 2, 3, ...
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_get_price_response_for_debug1_2({sell_order_sub_seq=}) ▶ {res_df.shape=} ")  
                    ######################################################################################## DEBUG(1_2) DEBUG(1_2) DEBUG(1_2) : update_order()        
    
    
                    ####################################################################################### DEBUG(2) DEBUG(2) DEBUG(2) : update_order()
                    if not self.coin.real_mode:
                        if self.coin.debug_mode_debug2:
                            # ★ create outstanding sell order sub depending on get_price_count ★
                            # get sell 'get_price_count'
                            find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                            dfx = sell_sub_df[find_mask]
                            # found sell order sub ?
                            if dfx.shape[0] > 0:
                                ix = dfx.index[0]                                    
                                get_price_count = sell_sub_df.loc[ix, 'get_price_count'] # sell_sub_df.loc[find_mask, 'get_price_count'].values[0]   # 0,1,2,3,4,5,...
                                # update_get_price_response_for_debug2(res_df: pd.DataFrame, get_price_count: int) -> pd.DataFrame:
                                res_df = self.trade.update_get_price_response_for_debug2(res_df, get_price_count)                                
                                if res_df.shape[0] == 0:
                                    status = 9                          # set internal status code
                                    msg_code = 'ERR-999'                # null => ERR-999
                                    msg_str = 'DataFrame is empty'      # null => DataFrame is empty  
                                else:
                                    status = 0
                                    msg_code = 'OK'
                                    msg_str = 'Normal Return'                                                            
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_get_price_response_for_debug2({get_price_count=}) ▶ {status=}, {res_df.shape=} ")         
                    ###################################################################################### DEBUG(2) DEBUG(2) DEBUG(2) : update_order()
                 
                    # get_price() error ?
                    if status != 0:     
                        if status == 9: # pending exe_sell_close_order_limit() request ? 
                            # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}get_price({sub_sell_price:,.{_p_}f}){Bcolors.ENDC} ▶ {status=} pending exe_sell_close_order_limit() request: continue to next sell order sub... ") 
                            status = 0  # reset status code
                            continue    # continue to next sell order sub                  
                        else: # status == 1 'ERR-5008' Timestamp for this request is too late
                            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_price({sub_sell_price:,.{_p_}f}) ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str} {Bcolors.ENDC} ")                               
                            continue    # continue to next sell order sub 
                        # end of if status == 9: 
                    else:   # normal return => executed exe_sell_close_order_limit() request                
                        pass    # self.debug.sound_alert(1) 
                    # end of if status != 0: 
    
                    ### no error : res_df.shape[0] >= 1 (single or multiple rows)  
     
                    ####################################################################
                    ### 【4】UPDATE A SELL ORDER SUB (real_qty, real_price, real_fee) 
                    ####################################################################
    
                    # res_df = get_price() response for exe_sell_close_order_limit()         
                    # ----------------------------------------------------------------  
                    #  #   Column       Dtype              
                    # ----------------------------------------------------------------              
                    #  0   executionId  object             
                    #  1   fee          float64            
                    #  2   lossGain     float64            
                    #  3   orderId      object  (non unique id => same id)
                    #  4   positionId   object  (unique id)                     
                    #  5   price        float64            
                    #  6   settleType   object  (OPEN or CLOSE)             
                    #  7   side         object  (BUY or SELL)           
                    #  8   size         float64            
                    #  9   symbol       object  (BTC_JPY)           
                    # 10   timestamp    datetime64[ns, UTC]
                    # ----------------------------------------------------------------                  
    
                    ### update a sell order sub ★             
                    sub_sum_real_qty  = round(res_df.groupby('orderId')['size'].sum().values[0], _q_)           # round(999.12, _q_)
                    sub_sum_real_fee  = round(res_df.groupby('orderId')['fee'].sum().values[0], 0)              # zero(0) 
                    sub_mean_real_price = round(res_df.groupby('orderId')['price'].mean().values[0], _p_)       # round(999999.123, _p_)
                              
                    # DO NOT USED: self.csv.update_sell_order_sub(sell_time, sub_order_id, symbol, col_name_list, col_value_list) 
                    find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                    dfx = sell_sub_df[find_mask]
                    # not found sell order sub ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found: {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order sub => update sell order sub (real_qty, real_price, real_fee) 
                        sell_sub_df.loc[find_mask, 'real_qty']   = sub_sum_real_qty  
                        sell_sub_df.loc[find_mask, 'real_price'] = sub_mean_real_price 
                        sell_sub_df.loc[find_mask, 'real_fee']   = sub_sum_real_fee 
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub({sub_sell_order_id=}, real_qty={sub_sum_real_qty:,.{_q_}f}, real_price={sub_mean_real_price:,.{_p_}f}, real_fee={sub_sum_real_fee:,.0f}) ")   
                    # end of if dfx.shape[0] == 0:  
    
                    ##############################################################################################################################
                    ### 【5】ADD A SELL ORDER CHILD (sell_time, sell_order_id, real_qty, real_price, real_fee) + (buy_time, buy_order_id)
                    ##############################################################################################################################
                      
                    ### check exit condition : sub_sell_qty=sub_sell_qty, sub_sum_real_qty=sum(res_df['size'])          
    
                    # ######################################### TEMP TEMP TEMP
                    # temp_sell_qty = round(sub_sell_qty, _q_)
                    # temp_real_qty = round(sub_sum_real_qty, _q_)
                    # self.debug.trace_warn(f".... DEBUG: {temp_sell_qty=:,.{_q_}f}, {temp_real_qty=:,.{_q_}f} ")
                    # ######################################## TEMP TEMP TEMP        
    
                    # completed exe_sell_close_order_limit() request ?
                    if round(sub_sell_qty, _q_) == round(sub_sum_real_qty, _q_):                                                
                        # changed open => closed  
                        if self.coin.real_mode:                                   
                            self.debug.sound_alert(1)
    
                        # increment sell count
                        self.gvar.sell_count += 1  
                        self.coin.sell_count += 1        
           
                        # response df for get_price(): exe_sell_close_order_limit()
                        for k in range(len(res_df)):
                            sell_order_id   = res_df.loc[k, 'orderId']      # sell order id (unique id)
                            position_id     = res_df.loc[k, 'positionId']   # position id (non unique id => same id)
                            real_qty        = res_df.loc[k, 'size']         # real qty
                            real_price      = res_df.loc[k, 'price']        # real price
                            real_fee        = res_df.loc[k, 'fee']          # real fee         
    
                            if not self.coin.real_mode:
                                position_id = sub_position_id                        
            
                            ### write a sell order child ★      
                            # write_sell_order_child(sell_time: utcnow, sell_order_id: str, buy_time: utcnow, buy_order_id: str, position_id: str, qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:               
                            _qty_ = real_qty; _price_ = real_price
                            self.csv.write_sell_order_child(sub_sell_time, sell_order_id, sub_buy_time, sub_buy_order_id, position_id, _qty_, _price_, real_qty, real_price, real_fee)    
                        # end of for k in range(len(res_df)):                   
                                                                       
                        # ### update sell order sub (bought=True) ★ DO NOT USE update_sell_order_sub() : SUSPEND SUSPEND SUSPEND                
                                                                                                                     
                    else:   # outstanding sell_qty exists => incomplete status (sell order / sell order sub will be updated by update_order() 
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} incomplete sell order sub({sub_sell_order_id}) ▶ sell_qty={sub_sell_qty:,.{_q_}f}, sell_real_qty={sub_sum_real_qty:,.{_q_}f} ")    
                        # pass    # skip (do nothing)    
                    # end of if sub_sell_qty == total_real_qty:
    
                    # continue to next sell order sub
                # end of for j in range(len(sell_subx_df)):
    
                #################################################################################
                ### 【6】SAVE BUY/SELL ORDER SUB TO CSV FILES
                #################################################################################
    
                ### CLOSE (save the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
                csv_file = self.folder_trading_type + f'buy_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/buy_order_sub(BTC_JPY).csv  
                buy_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode   
                                        
                ### CLOSE (save the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
                csv_file = self.folder_trading_type + f'sell_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/sell_order_sub(BTC_JPY).csv  
                sell_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode               
    
                #################################################################################
                ### 【7】UPDATE SELL ORDER (real_qty, real_price, real_fee)
                #################################################################################     
    
                ### calculate total sell qty / fee and mean price ★ sell_time ★
                filter_mask = sell_sub_df['sell_time'] == sell_time
                dfx = sell_sub_df[filter_mask]    
                # not found sell order sub ?
                if dfx.shape[0] == 0:
                    self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL} sell order sub({sell_time:%Y-%m-%d %H:%M:%S}) not found: {dfx.shape=}{Bcolors.ENDC} ")   
                else: # found sell order sub                        
                    header_sum_real_qty = dfx.groupby('sell_time')['real_qty'].sum().values[0]    
                    header_sum_real_fee = dfx.groupby('sell_time')['real_fee'].sum().values[0] 
                    header_mean_real_price = dfx.groupby('sell_time')['real_price'].mean().values[0]                  
    
                    header_sum_real_qty = round(header_sum_real_qty, _q_)         # round(990.12, _q_)     
                    header_sum_real_fee = round(header_sum_real_fee, 0)           # zero(0)
                    header_mean_real_price = round(header_mean_real_price, _p_)   # round(999999.123, _p_)          
    
                    ### update sell order (real_qty, real_price, real_fee) ★
                    find_mask = sell_df['sell_time'] == sell_time
                    dfx = sell_df[find_mask]
                    # not found sell order ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL} sell order({sell_time:%Y-%m-%d %H:%M:%S}) not found: {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order => update 
                        sell_df.loc[find_mask, 'real_qty']   = header_sum_real_qty                  
                        sell_df.loc[find_mask, 'real_price'] = header_mean_real_price 
                        sell_df.loc[find_mask, 'real_fee']   = header_sum_real_fee 
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={header_sum_real_qty:,.{_q_}f}, real_price={header_mean_real_price:,.{_p_}f}, real_fee={header_sum_real_fee:,.0f}) ")                                                    
                    # end of if dfx.shape[0] == 0:          
                # end of if dfx.shape[0] == 0:  
           
                # continue to next sell order... ★
            # end of for i in range(len(sell_df)):
    
            #################################################################################
            ### 【8】SAVE BUY/SELL ORDER TO CSV FILES 
            #################################################################################        
    
            ### CLOSE (save the buy order csv file) : buy_order(BTC_JPY).csv         
            csv_file = self.folder_trading_type + f'buy_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/buy_order(BTC_JPY).csv  
            buy_df.to_csv(csv_file, header=False, index=False) # OverWrite mode  
    
            ### CLOSE (save the sell order csv file) : sell_order(BTC_JPY).csv      
            csv_file = self.folder_trading_type + f'sell_order({symbol}).csv' 
            # desktop-pc/bot//buy_sell/sell_order(BTC_JPY).csv   
            sell_df.to_csv(csv_file, header=False, index=False) # OverWrite mode 
           
            return            
        
        ################################################
        def update_partially_closed_order(self) -> None: 
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal   
            self.gvar.callers_method_name = ':: update_partially_closed_order():'
            order_completed = self.trade.get_order_status(exclude=False)   # include stop_loss, cancelled, closed order info
            
            df = self.coin.master2_df    
            mas_df = pd.DataFrame(df)   # cast df (master dataframe)
            # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}last order completed({order_completed}){Bcolors.ENDC}, mas_df.shape({mas_df.shape[0]}), cancel({self.gvar.cancel_pending_orders}) ")       
    
            # mas_df is empty ? => SYTEM ERROR
            if mas_df.shape[0] == 0: 
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - mas_df({mas_df.shape[0]}) is empty. skip update_order() ")
                self.debug.sound_alert(3)                  
                return 
    
            # ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            # buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            # ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            # buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)       
    
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
    
            # get_orderbooks_try() -> tuple:  # return (sell_df, buy_df)   
            sell_bk, buy_bk = self.api.get_orderbooks_try()
            # sleep(0.5)
            sell_bk_df = pd.DataFrame(sell_bk)  # ascending order of price [-1, price]      : low to high price 100, 110, 120, 130, 140, 150
            buy_bk_df = pd.DataFrame(buy_bk)    # descending order of price iloc[0, price]  : high to low price 150, 140, 130, 120, 110, 100
            
            # not empty order books ?
            if sell_bk_df.shape[0] > 0 and buy_bk_df.shape[0] > 0:                
                # get latest sell/buy price from the order books
                bk_sell_price = sell_bk_df.iloc[0]['price'] # get lowest sell price : 100
                bk_buy_price = buy_bk_df.iloc[0]['price']   # get highest buy price : 150
            else:
                bk_sell_price = 0.0
                bk_buy_price = 0.0
    
            #################################################
            ### 【1】READ ALL SELL ORDER 
            #################################################
    
            ### get all sell order 
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']   # unique time                  
                buy_time        = sell_df.loc[i, 'buy_time']    # unique time                        
                sell_qty        = sell_df.loc[i, 'qty']         
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']    
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True        
                     
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell order sub
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell order sub ?
                    if dfx.shape[0] > 0:
                        dfx.reset_index(inplace=True)                
                        sell_order_id = str(dfx.loc[0, 'sell_order_id'])    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sell_order_id})... ")                        
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_order_id}) has been closed: sell_order_closed={sell_order_closed} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_time:%Y-%m-%d %H:%M:%S}) has been closed: sell_order_closed={sell_order_closed} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # continue to next sell order     
                # end of if sell_order_closed:    
    
                ### open or incomplete sell order
    
                ##########################################################################################
                ### 【2】READ ALL SELL ORDER SUB & EXECUTE GET_PRICE() FOR EACH SELL ORDER SUB
                ##########################################################################################
    
                ### for sell order sub : use pandas dataframe groupby()            
    
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order : 2 rows
                for j in range(len(sell_subx_df)):   # ★ sell_subx_df ★
                    sell_order_sub_seq = j + 1 # 1, 2, 3,....
    
                    ### get sell order sub info
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # S9999-999999-999999 ★ (unique id) used for get_price() api
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # B9999-999999-999999 ★ (non unique id)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # (unique id)
                    sub_sell_qty        = sell_subx_df.loc[j, 'qty']                # 10
                    sub_sell_price      = sell_subx_df.loc[j, 'price']            
                    sub_sell_real_qty   = sell_subx_df.loc[j, 'real_qty']           # 10
                    sub_sell_real_price = sell_subx_df.loc[j, 'real_price']            
                    sub_sell_real_fee   = sell_subx_df.loc[j, 'real_fee']  
                    sub_sell_stop_loss  = sell_subx_df.loc[j, 'stop_loss'] 
                    sub_sell_cancelled  = sell_subx_df.loc[j, 'cancelled']        
    
                    sub_sell_order_open = False
                    sub_sell_order_closed = False
                    sub_sell_order_incomplete = False
    
                    if sub_sell_real_qty == 0:
                        sub_sell_order_open = True
                    elif sub_sell_qty == sub_sell_real_qty:
                        sub_sell_order_closed = True
                    else:
                        sub_sell_order_incomplete = True                             
    
                    # fake sell sub order ? => SKIP
                    if sub_sell_order_id.startswith('Fake'): 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id})... ") 
                        continue             
                        
                    # cancelled sell sub order ? => SKIP
                    if sub_sell_cancelled:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled ▶ sub_sell_cancelled={sub_sell_cancelled} ")  
                        continue
    
                    # stop loss sell sub order ? => SKIP
                    if sub_sell_stop_loss:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss ▶ sub_sell_stop_loss={sub_sell_stop_loss} ")  
                        continue            
    
                    # closed sell sub order ? => SKIP 
                    if sub_sell_order_closed:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed ▶ sub_sell_order_closed={sub_sell_order_closed} ")  
                        continue     
    
                    ### open or incomplete sell order sub
     
                    ##########################################################################################
                    ### 【3】GET ORDER STATUS
                    ##########################################################################################
    
                    ### get order_status
                    qty = sub_sell_qty
                    price = sub_sell_price
    
                    # get_order_status_try(order_id: str, qty: float, price: float) ->tuple:    # return (status, order_status)                  
                    status, order_status = self.api.get_order_status_try(sub_sell_order_id, qty, price)     # ★ qty, price are used for fake mode only 
                    # status: 0(ok), 4(invalid request), 8(retry error), 9(internal error), ?(misc error) 
    
                    ### status = 0; order_status = 'EXPIRED'    # DEBUG DEBUG DEBUG
    
                    # get_order_status() error ?
                    if status != 0:                       
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}) error ▶ {Bcolors.FAIL} {status=}, {order_status=} {Bcolors.ENDC} ")                               
                        continue    # continue to next sell order sub                       
    
                    ### get_order_status() : status == 0, order_status in 'WAITING,MODIFYING,CANCELLING,ORDERED,EXECUTED,EXPIRED,CANCELED'
    
                    # pending exe_sell_close_order_limit() request ? : ORDERED(指値注文有効中), EXECUTE(全量約定)
                    if order_status in 'WAITING,MODIFYING,CANCELLING,ORDERED':                               
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}get_order_status_try({sub_sell_order_id}, {qty:,.{_q_}f}, {price:,.{_p_}f}){Bcolors.ENDC} ▶ {order_status=} pending exe_sell_close_order_limit() request ")                         
                        continue    # continue to next sell order sub                  
            
                    ### executed exe_sell_close_order_limit() : order_status in 'EXECUTED(全量約定),EXPIRED(全量失効|一部失効),CANCELED'       
    
                    if order_status in 'EXPIRED,CANCELED':                      
                        ### update sell order, sell order sub and order csv file
                        if order_status == 'CANCELED':
                             self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}): {Bcolors.FAIL}the order has been cancelled by GMO. please confirm the reason{Bcolors.ENDC} ")   
                             self.debug.sound_alert(3)
                             continue    # continue to next sell order sub 
    
                        if order_status == 'EXPIRED':     
                            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_order_status_try({sub_sell_order_id}): {Bcolors.FAIL}the order has been expired due to loss cut by GMO{Bcolors.ENDC} ")   
                            self.debug.sound_alert(3)
                    
                            ###########################################    
                            ### 1: close sell order sub
                            ###########################################                          
    
                            # update a sell order sub (update real_qty, real_price, stop_loss)                                             
                            # DO NOT USE: self.csv.update_sell_order_sub(sell_time, sub_order_id, symbol, col_name_list, col_value_list) 
                            find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                            dfx = sell_sub_df[find_mask]
                            # not found sell order sub ?
                            if dfx.shape[0] == 0:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found: {dfx.shape=}{Bcolors.ENDC} ")   
                            else: # found sell order sub => update sell order sub (real_qty, real_price, real_fee, stop_loss) 
                                gmo_loss_cut_price = 9_999_999.0
                                gmo_loss_cut_fee = 999.0
                                sell_stop_loss = True                            
                                sell_sub_df.loc[find_mask, 'real_qty']   = sub_sell_qty  
                                sell_sub_df.loc[find_mask, 'real_price'] = gmo_loss_cut_price   # gmo loss cut rate 
                                sell_sub_df.loc[find_mask, 'real_fee']   = gmo_loss_cut_fee     # gmo loss cut fee
                                sell_sub_df.loc[find_mask, 'stop_loss'] = sell_stop_loss
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub({sub_sell_order_id=}, real_qty={sub_sell_qty:,.{_q_}f}, {gmo_loss_cut_price=:,.{_p_}f}, {gmo_loss_cut_fee=:,.0f}, {sell_stop_loss=}) ")                                                          
                             # end of if dfx.shape[0] == 0:  
     
                            #######################################
                            ### 2: close sell order    
                            #######################################                              
    
                            # update sell order (real_qty, real_price, real_fee) 
                            find_mask = sell_df['sell_time'] == sell_time
                            dfx = sell_df[find_mask]
                            # not found sell order ?
                            if dfx.shape[0] == 0:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL} ({sell_time=:%Y-%m-%d %H:%M:%S}) not found: {dfx.shape=}{Bcolors.ENDC} ")   
                            else: # found sell order 
                                gmo_loss_cut_price = 9_999_999.0
                                gmo_loss_cut_fee = 999.0                            
                                sell_df.loc[find_mask, 'real_qty']   = sell_qty                 
                                sell_df.loc[find_mask, 'real_price'] = gmo_loss_cut_price   # gmo loss cut rate 
                                sell_df.loc[find_mask, 'real_fee']   = gmo_loss_cut_fee     # gmo loss cut fee 
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={sell_qty:,.{_q_}f}, {gmo_loss_cut_price=:,.{_p_}f}, {gmo_loss_cut_fee=:,.0f}) ")                                                    
                            # end of if dfx.shape[0] == 0:                                                      
    
                            ###########################################
                            ### 3: update order csv (update stop_loss)    
                            ###########################################  
                             
                            ### OPEN (load the order csv csv file) : order(BTC_JPY).csv 
                            ord_df = self.csv.get_order_csv()                 
                            if ord_df.shape[0] > 0:
                                # find the order csv by buy_order_id        
                                find_mask = ord_df['buy_order_id'] == sub_buy_order_id
                                dfx = ord_df[find_mask]
                                if dfx.shape[0] > 0:                                
                                    # update_order_csv_buy(buy_time: utcnow, buy_order_id: str, col_name_list: list, col_value_list: list) -> None: # PK(buy_time + buy_order_id)
                                    self.csv.update_order_csv_buy(sub_buy_time, sub_buy_order_id, col_name_list=['stop_loss'], col_value_list=[True])                                                    
    
                            continue    # continue to next sell order sub                  
                        # end of if order_status == 'EXPIRED':     
                    # end of if order_status in 'EXPIRED,CANCELED':                
    
    
                    ### get highest buy price from buy orderbooks
                    latest_book_price = bk_buy_price    # bk_buy_price = buy_bk_df.iloc[0]['price']    
    
                    ##########################################################################################
                    ### 【4】GET ORDER PRICE
                    ##########################################################################################
    
                    # ### get real qty, sell price, fee from the GMO
                    # ### get the latest close price from the GMO master              
                    # ### mas_latest_close_price = mas_df.iloc[-1]['close']   # get latest close price (GMO)              
    
                    _debug1_ = self.coin.debug_mode_debug1; _debug2_ = self.coin.debug_mode_debug2    
    
                    ######################################################################################### DEBUG(1), DEBUG(2)           
                    # _debug1_ = True; _debug2_ = True    # ★ TEMP TEMP TEMP ★   
                    # self.debug.trace_warn(f".... DEBUG _debug1_ = True; _debug2_ = True ")              
                    ######################################################################################### DEBUG(1), DEBUG(2)  
    
                    ### get sell real qty, price & fee from the GMO : get_price() for exe_sell_close_order_limit() ★     
                    
                    sleep(1)    # sleep 1 sec            
                    # get_price_try(order_id: str, qty: float, price: float, latest_book_price=0.0, debug1=False, debug2=False) -> tuple: 
                    status, res_df, msg_code, msg_str = self.api.get_price_try(sub_sell_order_id, sub_sell_qty, sub_sell_price, latest_book_price, _debug1_, _debug2_)  # ★ qty, price, close_price are used for fake mode                  
                    # status: 0(ok),  8(retry error), 9(empty dataframe or internal error), ?(misc error)
                    sleep(1)    # sleep 1 sec            
    
                    res_df = pd.DataFrame(res_df)   # cast dataframe
    
                    # not real mode and debug (stop loss or cancel pending orders) ?
                    ######################################################################################### DEBUG(1_2) DEBUG(1_2) DEBUG(1_2) : update_order()
                    if not self.coin.real_mode:
                        if self.coin.debug_mode_debug1: 
                            if res_df.shape[0] > 0:
                                # ★ create two sell order subs for each sell order header ★
                                # update_get_price_response_for_debug1_2(res_df: pd.DataFrame, sell_order_sub_seq: int) -> pd.DataFrame
                                res_df = self.trade.update_get_price_response_for_debug1_2(res_df, sell_order_sub_seq)  # 1, 2, 3, ...
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_get_price_response_for_debug1_2({sell_order_sub_seq=}) ▶ {res_df.shape=} ")  
                    ######################################################################################## DEBUG(1_2) DEBUG(1_2) DEBUG(1_2) : update_order()        
    
    
                    ####################################################################################### DEBUG(2) DEBUG(2) DEBUG(2) : update_order()
                    if not self.coin.real_mode:
                        if self.coin.debug_mode_debug2:
                            # ★ create outstanding sell order sub depending on get_price_count ★
                            # get sell 'get_price_count'
                            find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)   # PK(sell_time + sell_order_id) 
                            dfx = sell_sub_df[find_mask]
                            # found sell order sub ?
                            if dfx.shape[0] > 0:
                                ix = dfx.index[0]                                    
                                get_price_count = sell_sub_df.loc[ix, 'get_price_count'] # sell_sub_df.loc[find_mask, 'get_price_count'].values[0]   # 0,1,2,3,4,5,...
                                # update_get_price_response_for_debug2(res_df: pd.DataFrame, get_price_count: int) -> pd.DataFrame:
                                res_df = self.trade.update_get_price_response_for_debug2(res_df, get_price_count)                                
                                if res_df.shape[0] == 0:
                                    status = 9                          # set internal status code
                                    msg_code = 'ERR-999'                # null => ERR-999
                                    msg_str = 'DataFrame is empty'      # null => DataFrame is empty  
                                else:
                                    status = 0
                                    msg_code = 'OK'
                                    msg_str = 'Normal Return'                                                            
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} update_get_price_response_for_debug2({get_price_count=}) ▶ {status=}, {res_df.shape=} ")         
                    ###################################################################################### DEBUG(2) DEBUG(2) DEBUG(2) : update_order()
                 
                    # get_price() error ?
                    if status != 0:     
                        if status == 9: # pending exe_sell_close_order_limit() request ? 
                            # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.WARNING}get_price({sub_sell_price:,.{_p_}f}){Bcolors.ENDC} ▶ {status=} pending exe_sell_close_order_limit() request: continue to next sell order sub... ") 
                            status = 0  # reset status code
                            continue    # continue to next sell order sub                  
                        else: # status == 1 'ERR-5008' Timestamp for this request is too late
                            self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} get_price({sub_sell_price:,.{_p_}f}) ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str} {Bcolors.ENDC} ")                               
                            continue    # continue to next sell order sub 
                        # end of if status == 9: 
                    else:   # normal return => executed exe_sell_close_order_limit() request                
                        pass    # self.debug.sound_alert(1) 
                    # end of if status != 0: 
    
                    ### no error : res_df.shape[0] >= 1 (single or multiple rows)  
     
                    ##############################################################################################################################
                    ### 【5】ADD A SELL ORDER CHILD (sell_time, sell_order_id, real_qty, real_price, real_fee) + (buy_time, buy_order_id)
                    ##############################################################################################################################                       
    
                    # response df for get_price(): exe_sell_close_order_limit()
                    for k in range(len(res_df)):
                        sell_order_id   = res_df.loc[k, 'orderId']      # sell order id (unique id)
                        position_id     = res_df.loc[k, 'positionId']   # position id (non unique id => same id)
                        real_qty        = res_df.loc[k, 'size']         # real qty
                        real_price      = res_df.loc[k, 'price']        # real price
                        real_fee        = res_df.loc[k, 'fee']          # real fee         
    
                        if not self.coin.real_mode:
                            position_id = sub_position_id                        
        
                        ### write a sell order child ★      
                        # write_sell_order_child(sell_time: utcnow, sell_order_id: str, buy_time: utcnow, buy_order_id: str, position_id: str, qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:               
                        _qty_ = real_qty; _price_ = real_price
                        self.csv.write_sell_order_child(sub_sell_time, sell_order_id, sub_buy_time, sub_buy_order_id, position_id, _qty_, _price_, real_qty, real_price, real_fee)    
                    # end of for k in range(len(res_df)):     
    
                    # continue to next sell order sub
                # end of for j in range(len(sell_subx_df)):
                    
                # continue to next sell order... ★
            # end of for i in range(len(sell_df)):     
    
            return           
    
        ####################################################### 
        def update_order_profit(self, first_pass=True) -> None:
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal
    
            self.gvar.callers_method_name = 'update_order_profit():'
            order_completed = self.trade.get_order_status(exclude=False)   # include stop_loss, cancelled, closed order info
            self.debug.trace_write(f".... {self.gvar.callers_method_name} first_pass({first_pass}), last_order_completed({order_completed}) ")   
    
            ######################################################################################################################################################################
            ### 【1】IMPORT BUY ORDER SUB & APPEND/UPDATE ORDER CSV FILE (buy_time, buy_order_id, buy_real_qty, buy_real_price, buy_real_fee) & (sell_time, sell_order_id)
            ######################################################################################################################################################################
    
            ### OPEN (load the order csv file) : order(BTC_JPY).csv 
            ord_df = self.csv.get_order_csv()               
            
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv
            buy_sub_df = self.csv.get_buy_order_sub()         
    
            ### import buy order sub file       
    
            # get all buy order sub
            for i in range(len(buy_sub_df)):
                ### get buy order sub info
                sub_buy_time            = buy_sub_df.loc[i, 'buy_time']             # ★ (non unique)
                sub_sell_time           = buy_sub_df.loc[i, 'sell_time']            # ★ (non unique)
                sub_buy_order_id        = buy_sub_df.loc[i, 'buy_order_id']         # ★ 'B9999-999999-999999' (unique)
                sub_sell_order_id       = buy_sub_df.loc[i, 'sell_order_id']        # ★ 'S9999-999999-999999;S9999-999999-999999;S9999-999999-999999' (list of sell_order_ids) : buy sub(one) <= sell sub(many)
                sub_buy_qty             = buy_sub_df.loc[i, 'qty']
                sub_buy_price           = buy_sub_df.loc[i, 'price']
                sub_buy_real_qty        = buy_sub_df.loc[i, 'real_qty']                
                sub_buy_real_price      = buy_sub_df.loc[i, 'real_price']
                sub_buy_real_fee        = buy_sub_df.loc[i, 'real_fee']
                sub_buy_get_price_count = buy_sub_df.loc[i, 'get_price_count']    
                sub_buy_stop_loss       = buy_sub_df.loc[i, 'stop_loss']
                sub_buy_order_cancelled = buy_sub_df.loc[i, 'cancelled']
                sub_buy_order_closed = True if sub_buy_qty == sub_buy_real_qty else False                  
           
                # fake sub order ? => SKIP
                if sub_buy_order_id.startswith('Fake'): 
                    # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake buy order sub({sub_buy_order_id})... ") 
                    continue                              
    
                # empty order csv file ?
                if ord_df.shape[0] == 0:
                    dfx = pd.DataFrame()
                else:   # not empty dataframe => find the order csv file by buy_order_id
                    find_mask = ord_df['buy_order_id'] == sub_buy_order_id  # B9999-999999-999999
                    dfx = ord_df[find_mask]
                
                # not found order csv file ?
                if dfx.shape[0] == 0:                 
                    ### append order csv file ★IMPORTANT★ DO NOT APEEND 'Null' Value => sell_order_id = 'S9999-999999-999999'
                    columns = Gmo_dataframe.ORDER_CSV
                    _sell_real_qty_=0.0; _sell_real_price_=0.0; _sell_real_fee_=0.0; _real_profit_=0.0
                    _accumulated_leverage_fee_=0.0; _close_count_=0; _close_update_date_=dt.datetime.now()
                    _order_closed_ = False; _stop_loss_=sub_buy_stop_loss; _cancelled_=sub_buy_order_cancelled
                    _order_open_date_ = dt.datetime.now(); _order_close_date_ = dt.datetime.now(); _log_time_=dt.datetime.now()
                    data = [[sub_buy_time, sub_sell_time, sub_buy_order_id, sub_sell_order_id, sub_buy_real_qty, _sell_real_qty_, sub_buy_real_price, _sell_real_price_, sub_buy_real_fee, _sell_real_fee_, _real_profit_, _accumulated_leverage_fee_, _close_count_, _close_update_date_, _order_closed_, _stop_loss_, _cancelled_, _order_open_date_, _order_close_date_, _log_time_]] 
                    new_df = pd.DataFrame(data, columns=columns)                                           
    
                    # convert datatypes (buy_time, sell_time, log_time, buy_order_id, sell_order_id)
                    new_df['buy_time']  = pd.to_datetime(new_df['buy_time'], utc=True)          # object => utc time (aware)
                    new_df['sell_time'] = pd.to_datetime(new_df['sell_time'], utc=True)         # object => utc time (aware)   
                    new_df['close_update_date']  = pd.to_datetime(new_df['close_update_date'])  # object => ns time (jp time)   
                    new_df['order_open_date']    = pd.to_datetime(new_df['order_open_date'])    # object => ns time (jp time) 
                    new_df['order_close_date']   = pd.to_datetime(new_df['order_close_date'])   # object => ns time (jp time)                    
                    new_df['log_time']  = pd.to_datetime(new_df['log_time'])                    # object => ns time (jp time)    
    
                    new_df['buy_order_id']  = new_df['buy_order_id'].astype(str)                # int64 => string
                    new_df['sell_order_id'] = new_df['sell_order_id'].astype(str)               # int64 => string 
    
                    # append a new row (order row)
                    ord_buy_order_id = sub_buy_order_id
                    ord_buy_real_qty = sub_buy_real_qty
                    ord_buy_real_price = sub_buy_real_price
                    ord_buy_real_fee = sub_buy_real_fee
                    ord_order_closed = False
                    ord_df = ord_df.append(new_df, ignore_index=True)                                      
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} write_order_csv_buy({ord_buy_order_id=}, {ord_buy_real_qty=:,.{_q_}f}, {ord_buy_real_price=:,.{_p_}f}, {ord_buy_real_fee=:,.0f}, {ord_order_closed=}) ")                              
                else: # found order csv file => update order csv file
                    # get order csv info
                    ix = dfx.index[0]   # get index of the first row
                    # ord_buy_order_id = ord_df.loc[ix, 'buy_order_id']
                    # ord_buy_real_qty = ord_df.loc[ix, 'buy_real_qty']
                    # ord_sell_real_qty = ord_df.loc[ix, 'sell_real_qty']               
                    # order_closed = ord_df.loc[ix, 'order_closed']   
    
                    ord_buy_order_id    = ord_df.loc[ix, 'buy_order_id']
                    ord_sell_order_id   = ord_df.loc[ix, 'sell_order_id']
                    ord_buy_real_qty    = ord_df.loc[ix, 'buy_real_qty']
                    ord_sell_real_qty   = ord_df.loc[ix, 'sell_real_qty']      
                    ord_buy_real_price  = ord_df.loc[ix, 'buy_real_price']
                    ord_sell_real_price = ord_df.loc[ix, 'sell_real_price']      
                    ord_buy_real_fee    = ord_df.loc[ix, 'buy_real_fee']
                    ord_sell_real_fee   = ord_df.loc[ix, 'sell_real_fee']                          
                    ord_order_closed   = ord_df.loc[ix, 'order_closed']                  
                                  
                    # completed order csv file ?
                    if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and round(ord_buy_real_qty, _q_) == round(ord_sell_real_qty, _q_):
                        if not ord_order_closed:
                            # update order csv file 
                            ord_order_closed = True
                            ord_df.loc[find_mask, 'order_closed']   = ord_order_closed      
                            ord_df.loc[find_mask, 'order_close_date'] = dt.datetime.now() 
                            ord_df.loc[find_mask, 'log_time'] = dt.datetime.now()                            
    
                            ord_df['order_close_date']   = pd.to_datetime(ord_df['order_close_date'])  # object => ns time (jp time)                         
                            ord_df['log_time']  = pd.to_datetime(ord_df['log_time'])    # object => ns time (jp time)   
                            self.debug.trace_write(f".... {self.gvar.callers_method_name} update_order_csv_buy({ord_buy_order_id=}, {ord_buy_real_qty=:,.{_q_}f}, {ord_buy_real_price=:,.{_p_}f}, {ord_buy_real_fee=:,.0f}, {ord_order_closed=}) ")                         
                        # self.debug.trace_write(f".... update_order2(): pass update order info (buy): buy_order_id={sub_buy_order_id}, buy_real_qty={sub_buy_real_qty:,.{_q_}f}, buy_real_price={sub_buy_real_price:,.{_x_}f}, buy_real_fee={sub_buy_real_fee:,.0f}  ")                              
                    else: # incomplete order => update order csv file                                        
                        # update order csv 
                        ord_buy_time = sub_buy_time
                        ord_buy_order_id = sub_buy_order_id
                        ord_buy_real_qty = sub_buy_real_qty
                        ord_buy_real_price = sub_buy_real_price
                        ord_buy_real_fee = sub_buy_real_fee
    
                        ord_df.loc[find_mask, 'buy_time']       = ord_buy_time      
                        ord_df.loc[find_mask, 'buy_order_id']   = ord_buy_order_id
                        ord_df.loc[find_mask, 'buy_real_qty']   = ord_buy_real_qty                
                        ord_df.loc[find_mask, 'buy_real_price'] = ord_buy_real_price
                        ord_df.loc[find_mask, 'buy_real_fee']   = ord_buy_real_fee 
                        ord_df.loc[find_mask, 'log_time'] = dt.datetime.now()    
    
                        ### update order_closed flag
    
                        # get buy_real_qty and sell_real_qty
                        ix = dfx.index[0]   # get index of the first row
                        # ord_buy_order_id = ord_df.loc[ix, 'buy_order_id']
                        # ord_buy_real_qty = ord_df.loc[ix, 'buy_real_qty']
                        # ord_sell_real_qty = ord_df.loc[ix, 'sell_real_qty']               
                        # order_closed = ord_df.loc[ix, 'order_closed']          
                        if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and round(ord_buy_real_qty, _q_) == round(ord_sell_real_qty, _q_):  
                            ord_order_closed = True
                            ord_df.loc[find_mask, 'order_closed'] = ord_order_closed                                   
    
                        # convert datatypes (buy_time, sell_time, log_time, buy_order_id, sell_order_id)
                        ord_df['buy_time']  = pd.to_datetime(ord_df['buy_time'], utc=True)      # object => utc time (aware)
                        ord_df['sell_time'] = pd.to_datetime(ord_df['sell_time'], utc=True)     # object => utc time (aware)      
                        ord_df['close_update_date']  = pd.to_datetime(ord_df['close_update_date']) # object => ns time (jp time)   
                        ord_df['order_open_date']    = pd.to_datetime(ord_df['order_open_date'])   # object => ns time (jp time) 
                        ord_df['order_close_date']   = pd.to_datetime(ord_df['order_close_date'])  # object => ns time (jp time)                      
                        ord_df['log_time']  = pd.to_datetime(ord_df['log_time'])                # object => ns time (jp time)    
    
                        ord_df['buy_order_id']  = ord_df['buy_order_id'].astype(str)            # int64 => string
                        ord_df['sell_order_id'] = ord_df['sell_order_id'].astype(str)           # int64 => string                   
        
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_order_csv_buy({ord_buy_order_id=}, {ord_buy_real_qty=:,.{_q_}f}, {ord_buy_real_price=:,.{_p_}f}, {ord_buy_real_fee=:,.0f}, {ord_order_closed=}) ")                              
                    # end of if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and ord_buy_real_qty == ord_sell_real_qty:                             
                # end of if dfx.shape[0] == 0:
                # continue to next buy sub orders
            # end of for i in range(len(buy_sub_df)):	           
    		
            #######################################################################################################################################
            ### 【2】IMPORT SELL ORDER SUB & UPDATE ORDER CSV FILE (sell_time, sell_order_id, sell_real_qty, sell_real_price, sell_real_fee)
            #######################################################################################################################################
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv
            sell_sub_df = self.csv.get_sell_order_sub()	
    
            ### import sell order sub file      
    
            for i in range(len(sell_sub_df)):
                ### get sell order sub info
                sub_sell_time               = sell_sub_df.loc[i, 'sell_time']           # ★ (non unique)
                sub_buy_time                = sell_sub_df.loc[i, 'buy_time']            # ★ (non unique)
                sub_sell_order_id           = sell_sub_df.loc[i, 'sell_order_id']       # ★ 'S9999-999999-999999' (unique id)
                sub_buy_order_id            = sell_sub_df.loc[i, 'buy_order_id']        # ★ 'B9999-999999-999999' (non unique id) : sell sub(many) => buy_sub(one)
                sub_sell_qty                = sell_sub_df.loc[i, 'qty']
                sub_sell_price              = sell_sub_df.loc[i, 'price']
                sub_sell_real_qty           = sell_sub_df.loc[i, 'real_qty']
                sub_sell_real_price         = sell_sub_df.loc[i, 'real_price']
                sub_sell_real_fee           = sell_sub_df.loc[i, 'real_fee']
                sub_sell_get_price_count    = sell_sub_df.loc[i, 'get_price_count']     
                sub_sell_stop_loss          = sell_sub_df.loc[i, 'stop_loss']         
                sub_sell_order_cancelled    = sell_sub_df.loc[i, 'cancelled']
    
                sub_sell_order_open = False
                sub_sell_order_closed = False
                sub_sell_order_incomplete = False
    
                if sub_sell_real_qty == 0:
                    sub_sell_order_open = True
                elif sub_sell_qty == sub_sell_real_qty:
                    sub_sell_order_closed = True
                else:
                    sub_sell_order_incomplete = True       
    
                # fake sub order ? => SKIP
                if sub_sell_order_id.startswith('Fake'): 
                    # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id})... ") 
                    continue             
                                           
                # empty order csv file ?           
                if ord_df.shape[0] == 0:
                    self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}SYSTEM ERROR{Bcolors.ENDC} - order csv file is empty: continue... ")
                    self.debug.sound_alert(3)  
                    continue
    
                # find the order csv file by buy_order_id        
                find_mask = ord_df['buy_order_id'] == sub_buy_order_id
                dfx = ord_df[find_mask]
                # not found order csv file ?
                if dfx.shape[0] == 0:
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}ERROR{Bcolors.ENDC} order csv file not found: buy_order_id={sub_buy_order_id} ")  
                    self.debug.sound_alert(3)  
                    continue                
    
                ### found order csv file
                
                # get order csv info
                ix = dfx.index[0]   # get index of the first row
                ord_buy_order_id    = ord_df.loc[ix, 'buy_order_id']
                ord_sell_order_id   = ord_df.loc[ix, 'sell_order_id']
                ord_buy_real_qty    = ord_df.loc[ix, 'buy_real_qty']
                ord_sell_real_qty   = ord_df.loc[ix, 'sell_real_qty']      
                ord_buy_real_price  = ord_df.loc[ix, 'buy_real_price']
                ord_sell_real_price = ord_df.loc[ix, 'sell_real_price']      
                ord_buy_real_fee    = ord_df.loc[ix, 'buy_real_fee']
                ord_sell_real_fee   = ord_df.loc[ix, 'sell_real_fee']                          
                ord_order_closed    = ord_df.loc[ix, 'order_closed']   
    
                # self.debug.trace_warn(f".... DEBUG(1): {ord_buy_real_qty=}, {ord_sell_real_qty=}, {round(ord_buy_real_qty, _q_)=}, {round(ord_sell_real_qty, _q_)=} ")
                        
                # completed order csv file ?
                if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and round(ord_buy_real_qty, _q_) == round(ord_sell_real_qty, _q_): 
                    if not ord_order_closed:
                        # update order csv file 
                        ord_order_closed = True   
                        ord_df.loc[find_mask, 'order_closed'] = ord_order_closed      
                        ord_df.loc[find_mask, 'order_close_date'] = dt.datetime.now() 
                        ord_df.loc[find_mask, 'log_time'] = dt.datetime.now() 
                        
                        ord_df['order_close_date']   = pd.to_datetime(ord_df['order_close_date'])  # object => ns time (jp time)                         
                        ord_df['log_time']  = pd.to_datetime(ord_df['log_time'])    # object => ns time (jp time)  
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_order_csv_sell({ord_sell_order_id=}, {ord_sell_real_qty=:,.{_q_}f}, {ord_sell_real_price=:,.{_p_}f}, {ord_sell_real_fee=:,.0f}, {ord_order_closed=}) ")             
                    # self.debug.trace_write(f".... update_order2(): pass update order csv file (sell): sell_order_id={sub_sell_order_id}, sell_real_qty={sub_sell_real_qty:,.{_q_}f}, sell_real_price={sub_sell_real_price:,.{_x_}f}, sell_real_fee={sub_sell_real_fee:,.0f}  ")                              
                else: # incomplete order => update order csv file
    
                    ### get a list of sell_order_id from the sell order sub ★ buy_order_id ★
                    filter_mask = sell_sub_df['buy_order_id'] == sub_buy_order_id
                    dfx = sell_sub_df[filter_mask]    
                    sell_order_id_list = dfx.groupby('buy_order_id')['sell_order_id'].apply(list)[0]     
                    sell_order_ids_by_semicolon = ''
                    for k, order_id in enumerate(sell_order_id_list):
                        if k == 0:
                            sell_order_ids_by_semicolon += order_id 
                        else:
                            sell_order_ids_by_semicolon += ';' + order_id 
    
                    ### calculate total sell qty / fee and mean price ★ buy_order_id ★
                    sell_sum_real_qty = dfx.groupby('buy_order_id')['real_qty'].sum().values[0]    
                    sell_sum_real_fee = dfx.groupby('buy_order_id')['real_fee'].sum().values[0] 
                    sell_mean_real_price = dfx.groupby('buy_order_id')['real_price'].mean().values[0]                           
    
                    ### update order csv file sell info (sell_time, sell_order_id, sell_real_qty, sell_real_price, sell_real_fee) OK
                    ord_sell_real_qty = round(sell_sum_real_qty, _q_)                           # round(999.12, _q_)  
                    ord_sell_real_price = round(sell_mean_real_price, _p_)                      # round(999999.123, _p_)  
                    ord_sell_real_fee = round(sell_sum_real_fee, 0)                             # zero(0)  
    
                    # order completed ?
                    if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and round(ord_buy_real_qty, _q_) == round(ord_sell_real_qty, _q_): 
                        ord_order_closed = True   
                        ord_df.loc[find_mask, 'order_closed'] = ord_order_closed   
    
                    ord_df.loc[find_mask, 'sell_time']       = sub_sell_time    
                    ord_df.loc[find_mask, 'sell_order_id']   = sell_order_ids_by_semicolon      # 'OrderId1;OrderId2;OrderId3'                       
                    ord_df.loc[find_mask, 'sell_real_qty']   = ord_sell_real_qty                # round(sell_sum_real_qty, _q_)                 
                    ord_df.loc[find_mask, 'sell_real_price'] = ord_sell_real_price              # round(sell_mean_real_price, _p_)       
                    ord_df.loc[find_mask, 'sell_real_fee']   = ord_sell_real_fee                # round(sell_sum_real_fee, 0)              
    
                    # convert datatypes (buy_time, sell_time, log_time, buy_order_id, sell_order_id)
                    ord_df['buy_time']  = pd.to_datetime(ord_df['buy_time'], utc=True)          # object => utc time (aware)
                    ord_df['sell_time'] = pd.to_datetime(ord_df['sell_time'], utc=True)         # object => utc time (aware)   
                    ord_df['close_update_date']  = pd.to_datetime(ord_df['close_update_date'])  # object => ns time (jp time)     
                    ord_df['order_open_date']    = pd.to_datetime(ord_df['order_open_date'])    # object => ns time (jp time) 
                    ord_df['order_close_date']   = pd.to_datetime(ord_df['order_close_date'])   # object => ns time (jp time)                 
                    ord_df['log_time']  = pd.to_datetime(ord_df['log_time'])                    # object => ns time (jp time)    
    
                    ord_df['buy_order_id']  = ord_df['buy_order_id'].astype(str)                # int64 => string
                    ord_df['sell_order_id'] = ord_df['sell_order_id'].astype(str)               # int64 => string               
    
                    # self.debug.trace_warn(f".... DEBUG(2): {ord_buy_real_qty=}, {ord_sell_real_qty=}, {round(ord_buy_real_qty, _q_)=}, {round(ord_sell_real_qty, _q_)=} ")
    
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} update_order_csv_sell({ord_sell_order_id=}, {ord_sell_real_qty=:,.{_q_}f}, {ord_sell_real_price=:,.{_p_}f}, {ord_sell_real_fee=:,.0f}, {ord_order_closed=}) ")                              
    
                # end of if ord_buy_real_qty > 0 and ord_sell_real_qty > 0 and ord_buy_real_qty == ord_sell_real_qty:    
                # continue to next sell order sub
            # end of for i in range(len(sell_sub_df)):
    
            #######################################################################################################################################
            ### 【3】SAVE ORDER TO CSV/EXCEL FILES
            #######################################################################################################################################
    
            ### CLOSE (save to the order csv file) : order(BTC_JPY).csv
            if ord_df.shape[0] > 0:
                ### CLOSE (order csv file)
    
                csv_file = self.folder_trading_type + f'order({symbol}).csv'  
                # desktop-pc/bot//buy_sell/order(BTC_JPY).csv   
                ord_df.to_csv(csv_file, header=False, index=False) # overWrite the order file  
         
                ### SAVE (order excel file)
    
                # save to the order excel file
                excel_file = self.folder_trading_type + f'order({symbol}).xlsx'
                # desktop-pc/bot/buy_sell/order(BTC_JPY).xlsx   
    
                excel_df = ord_df.copy()
    
                # (1) convert time to string              
                excel_df['buy_time']  = excel_df['buy_time'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))  
                excel_df['sell_time'] = excel_df['sell_time'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))   
                excel_df['close_update_date']  = excel_df['close_update_date'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))         
                excel_df['log_time']  = excel_df['log_time'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))  
    
                # (2) drop real_profit column
                excel_df = excel_df.drop(['real_profit'], axis=1)            
    
                # (3) add new columns
                excel_df['buy(SUM)']    = '=ROUND(E2*G2,3)'                     # add a new column
                excel_df['sell(SUM)']   = '=ROUND(F2*H2,3)'                     # add a new column
                excel_df['profit(AMT)'] = '=IF((H2-G2)>0,ROUNDDOWN((H2 - G2) * F2, 0),ROUNDUP((H2 - G2) * F2, 0))'  # add a new column : rounddown(profit), roundup(loss)
                excel_df['profit(%)']   = '=M2/K2'                              # add a new column
    
                # (4) update excel formula
    
                # get all excel order info  
                row = 2
                for i in range(len(excel_df)):
                    order_closed = excel_df.loc[i, 'order_closed']              # True or False
                    stop_loss = excel_df.loc[i, 'stop_loss']                    # True or False
                    cancelled = excel_df.loc[i, 'cancelled']                    # True or False
                    if order_closed or stop_loss or cancelled:
                        cell = str(row)
                        excel_df.loc[i, 'buy(SUM)']     = f'=ROUND(E{cell}*G{cell},3)'                     
                        excel_df.loc[i, 'sell(SUM)']    = f'=ROUND(F{cell}*H{cell},3)'                     
                        excel_df.loc[i, 'profit(AMT)']  = f'=IF((H{cell}-G{cell})>0,ROUNDDOWN((H{cell} - G{cell}) * F{cell}, 0),ROUNDUP((H{cell} - G{cell}) * F{cell}, 0))'  
                        excel_df.loc[i, 'profit(%)']    = f'=M{cell}/K{cell}'                   
                    else:
                        excel_df.loc[i, 'buy(SUM)']     = ''                     
                        excel_df.loc[i, 'sell(SUM)']    = ''                     
                        excel_df.loc[i, 'profit(AMT)']  = ''
                        excel_df.loc[i, 'profit(%)']    = ''                       
                    row += 1
                # end of for j in range(len(excel_df)):    
    
                # (5) re-order columns
                columns = [
                    'buy_time','sell_time','buy_order_id','sell_order_id','buy_real_qty','sell_real_qty','buy_real_price','sell_real_price','buy_real_fee','sell_real_fee',
                    'buy(SUM)','sell(SUM)','profit(AMT)','profit(%)',
                    'accumulated_leverage_fee','close_count','close_update_date',
                    'order_closed','stop_loss','cancelled','order_open_date','order_close_date','log_time'
                    ]    
                       
                excel_df = excel_df[columns]
            
                # print('excel_df.info()\n', excel_df.info())
    
                # ------------------------------------------------------------------------------------------
                #  #   Column                Dtype
                # ------------------------------------------------------------------------------------------
                #  1   time(B)               datetime64[utc]    => Convert to string
                #  2   time(S)               datetime64[utc]    => Convert to string
                #  3   order(B)              object  
                #  4   order(S)              object    
                #  5   qty(B)                float64    
                #  6   qty(S)                float64                
                #  7   price(B)              float64
                #  8   price(S)              float64
                #  9   fee(B)                float64
                # 10   fee(S)                float64
                #      real_profit           float64             => REMOVE
                # 11   buy(SUM)              float64             => ADD =ROUNDUP(E2*G2,3)+I2
                # 12   sell(SUM)             float64             => ADD =ROUNDDOWN(F2*H2,3)+J2
                # 13   profit(AMT)           float64             => ADD =L2-K2             
                # 14   profit(%)             float64             => ADD =M2/K2  
                # 15   fee(L)                float64                       
                # 16   count(L)              int                 
                # 17   close(L)              datetime64[ns]            
                # 18   order_closed          bool                 
                # 19   stop                  bool
                # 20   cancel                bool
                # 21   open_date             datetime64[ns]      
                # 22   close_date            datetime64[ns]      
                # 23   log_time              datetime64[ns]      => Convert to string
                # ----------------------------------------------------------------------------------------                              
    
                # (5) rename column names & export to excel file order(BTC_JPY).xlsx
                excel_df.columns = Gmo_dataframe.ORDER_XLSX
                excel_df.to_excel(excel_file, sheet_name='GMO Orders', index=False) 
            # end of if ord_df.shape[0] > 0:
    
            return    
    
        ############################################ 
        def stop_loss(self, method_id: int) -> None:
            """
            
            """
            
            if method_id <= 1:
                eval(f'self.stop_loss_{str(method_id)}()')    # call 'self.stop_loss_?() method
            else:
                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}Invalid stoploss_method_id {method_id=} {Bcolors.ENDC} -  skip stop_loss() ")
                self.debug.sound_alert(3)           
            
            # if method_id == 0:
            #     self.stop_loss_0()  # stop loss method 0
            # elif method_id == 1:
            #     self.stop_loss_1()  # stop loss method 1
            # else:   #
            #     self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} ▶ {Bcolors.FAIL}Invalid stoploss_method_id {method_id=} {Bcolors.ENDC} -  skip stop_loss() ")
            #     self.debug.sound_alert(3)    
    
            return    
    
        ############################## 
        def stop_loss_0(self) -> None:
            """
            
            """
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal
            self.gvar.callers_method_name = 'stop_loss(0):'        
            df = self.coin.master2_df
            mas_df = pd.DataFrame(df)   # cast df (master dataframe)
            
            self.debug.trace_write(f".... {self.gvar.callers_method_name} mas_df.shape({mas_df.shape[0]}), loss cut: 18%({self.coin.buy_sell_action14}), 15%({self.coin.buy_sell_action13}), 10%({self.coin.buy_sell_action12}) ")                               
    
            # ### OPEN (load the order csv file) : order(BTC_JPY).csv 
            # ord_df = self.csv.get_order_csv()          
          
            ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)  
           
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
         
            #################################################
            ### 【1】READ ALL SELL ORDERs 
            #################################################    
    
            # define sell order list
            header_real_qty_list = []
            header_real_price_list = []
            header_real_fee_list = []        
    
            ### get all sell orders   
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']             # ★ unique time 
                buy_time        = sell_df.loc[i, 'buy_time']              # ★ unique time   
                sell_qty        = sell_df.loc[i, 'qty']
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True  
    
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell order sub
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell order sub ?
                    if dfx.shape[0] > 0:
                        ix = dfx.index[0]               
                        sell_order_id = str(dfx.loc[ix, 'sell_order_id'])    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake order sub({sell_order_id})... ")
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sell_order_id}) has been closed: {sell_order_closed=} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{BBcolors.ENDC} sell order({sell_time:%Y-%m-%d %H:%M:%S}) has been closed: ={sell_order_closed=} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # skip closed order                                                            
                # end of if sell_order_closed:
                    
                #################################################################
                ### 【2】READ SELL ORDER SUB (open or incomplete status)
                #################################################################
                         
                ### for sell order sub (open or incomplete status)
    
                # define sell order sub list
                sub_real_qty_list = []
                sub_real_price_list = []
                sub_real_fee_list = []         
    
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order 
                for j in range(len(sell_subx_df)):  # ★ sell_subx_df ★   
                    ### get sell order sub info
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # S9999-999999-999999 ★ (unique id)     
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # B9999-999999-999999 ★ (non unique id) : sell sub(many) => buy_sub(one)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # ★ (unique id)
                    sub_qty             = sell_subx_df.loc[j, 'qty']                # 40  
                    sub_price           = sell_subx_df.loc[j, 'price']            
                    sub_real_qty        = sell_subx_df.loc[j, 'real_qty']           # 20         
                    sub_real_price      = sell_subx_df.loc[j, 'real_price']            
                    sub_real_fee        = sell_subx_df.loc[j, 'real_fee']         
                    sub_get_price_count = sell_subx_df.loc[j, 'get_price_count']   
                    sub_stop_loss       = sell_subx_df.loc[j, 'stop_loss']
                    sub_cancelled       = sell_subx_df.loc[j, 'cancelled']  
    
                    sub_order_open = False
                    sub_order_closed = False
                    sub_order_incomplete = False
    
                    if sub_real_qty == 0:
                        sub_order_open = True
                    elif sub_qty == sub_real_qty:
                        sub_order_closed = True
                    else:
                        sub_order_incomplete = True                             
    
                    # fake sell sub order ? => SKIP
                    if sub_sell_order_id.startswith('Fake') :
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id}) ")  
                        continue   
    
                    # sell sub order has been stop loss ? => SKIP
                    if sub_stop_loss: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss: {sub_stop_loss=} ")  
                        continue
    
                    # sell sub order has been cancelled ? => SKIP
                    if sub_cancelled: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled: {sub_cancelled=} ")  
                        continue
    
                    # closed sell sub order ? => SKIP
                    if sub_order_closed:
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed: {sub_order_closed=} ")  
                        continue  
    
                    #######################################################################################################
                    ### 【3】CHECK STOP LOSS CONDITIONS FOR EACH SELL ORDER SUB (open or incomplte sell order sub)
                    #######################################################################################################
    
                    ### open or incomplete sell order sub             
                     
                    stop_loss_detected = False
    
                    while True:
    
                        #####################################################
                        ### Check stop loss debug mode
                        #####################################################
                        if self.coin.debug_mode_stop_loss:
                            stop_loss_detected = True
                            break # exit while true loop 
    
                        #####################################################
                        ### Unconditional stop loss
                        #####################################################
    
                        ### check loss cut price : buy => sell 
    
                        ### calculate stop loss amount
    
                        stop_loss_amt = 0
                        latest_sell_price = 0
    
                        filter_mask = mas_df['side'] == 'SELL'
                        dfx = mas_df[filter_mask]        
                        if dfx.shape[0] > 0:          
                            # get latest sell price from GMO master dataframe
                            sell_price = dfx.iloc[-1]['price']
                            latest_sell_price = sell_price
    
                            # get last buy qty & price from the buy order header
                            qty = buy_df.iloc[-1]['qty']
                            buy_price = buy_df.iloc[-1]['real_price']   # get a real buy price
    
                            # calculate a stop loss amount : stop_loss_amt = math.floor((sell_price - buy_price) * qty)
                            stop_loss_amt = math.floor((sell_price - buy_price) * qty)  # positive => profit, negative => loss
                            stop_loss_amt = math.floor(stop_loss_amt) # positive => profit, negative => loss                    
    
                        ### calculate loss cut price from the buy sub order : loss_cut_price = last_buy_real_price - (last_buy_real_price * 0.20)                 
                        last_buy_real_price = buy_sub_df.iloc[-1]['real_price']      
    
                        ### buy_sell : sell position => calculate down limit for 5%, 10%, 15%, 18%, 20%, 23%, 25%(GMO loss cut rate)
    
                        # calculate loss cut price from the sell order sub  : loss_cut_price = last_buy_real_price - (last_buy_real_price * 0.20) 
    
                        loss_cut_price_05 = last_buy_real_price - (last_buy_real_price * 0.05) # 0.25(GMO loss cut rate) => 0.05 
                        loss_cut_price_10 = last_buy_real_price - (last_buy_real_price * 0.10) # 0.25(GMO loss cut rate) => 0.10             
                        loss_cut_price_15 = last_buy_real_price - (last_buy_real_price * 0.15) # 0.25(GMO loss cut rate) => 0.15 
                        loss_cut_price_18 = last_buy_real_price - (last_buy_real_price * 0.18) # 0.25(GMO loss cut rate) => 0.18 
                        loss_cut_price_20 = last_buy_real_price - (last_buy_real_price * 0.20) # 0.25(GMO loss cut rate) => 0.20 BOT LOSS CUT
                        loss_cut_price_23 = last_buy_real_price - (last_buy_real_price * 0.23) # 0.25(GMO loss cut rate) => 0.23 DO NOT USE THIS  
                        loss_cut_price_25 = last_buy_real_price - (last_buy_real_price * 0.25) # 0.25(GMO loss cut rate) => 0.25 GMO LOSS CUT
          
                        # gmo master latest sell price is greater than zero ?
                        if latest_sell_price > 0:   
    
                            if latest_sell_price <= loss_cut_price_25:      # <= 75
                                self.coin.buy_sell_lc25 += 1    
                                # self.debug.trace_warn(f".... DEBUG: loss cut 25%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_25%({loss_cut_price_25:,.{_x_}f}) ")                      
                            elif latest_sell_price <= loss_cut_price_23:    # <= 79
                                self.coin.buy_sell_lc23 += 1 
                                # self.debug.trace_warn(f".... DEBUG: loss cut 23%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_23%({loss_cut_price_23:,.{_x_}f}) ") 
                            elif latest_sell_price <= loss_cut_price_20:    # <= 80
                                self.coin.buy_sell_lc20 += 1  
                                # self.debug.trace_warn(f".... DEBUG: loss cut 20%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_20%({loss_cut_price_20:,.{_x_}f}) ")     
                            elif latest_sell_price <= loss_cut_price_18:    # <= 85
                                self.coin.buy_sell_lc18 += 1  
                                # self.debug.trace_warn(f".... DEBUG: loss cut 18%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_18%({loss_cut_price_18:,.{_x_}f}) ")                              
                            elif latest_sell_price <= loss_cut_price_15:    # <= 90
                                self.coin.buy_sell_lc15 += 1  
                                # self.debug.trace_warn(f".... DEBUG: loss cut 15%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_15%({loss_cut_price_15:,.{_x_}f}) ")                             
                            elif latest_sell_price <= loss_cut_price_10:    # <= 95
                                self.coin.buy_sell_lc10 += 1   
                                # self.debug.trace_warn(f".... DEBUG: loss cut 10%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_10%({loss_cut_price_10:,.{_x_}f}) ")                             
                            elif latest_sell_price <= loss_cut_price_05:    # <= 99
                                self.coin.buy_sell_lc5 += 1 
                                # self.debug.trace_warn(f".... DEBUG: loss cut 5%: latest_sell_price({latest_sell_price:,.{_x_}f}) <= loss_cut_5%({loss_cut_price_05:,.{_x_}f}) ")                                     
    
                            ### check down limit based on buy price
    
                            # GMO latest sell price is less than or equal to loss cut price (20%) ? => EXECUTE STOP LOSS 
                            if latest_sell_price <= loss_cut_price_20:
                                self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}STOP LOSS DETECTED REAL(1-5) EXECUTE{Bcolors.ENDC} loss cut 20%: latest_sell_price({latest_sell_price:,.{_p_}f}) <= loss_cut_20({loss_cut_price_20:,.{_p_}f})  ")                   
                                stop_loss_detected = True          
                                break # exit while true loop                           
                            
                        # end of if latest_sell_price > 0:            
                                   
                        break # exit while true loop 
                    # end of while True:     
    
                    if not stop_loss_detected: continue
    
                    #######################################################
                    # ★ STOP LOSS DETECTED ★ #                                        
                    #######################################################                     
                    
                    # increment stop loss count
                    self.gvar.stop_loss_count += 1
                    self.coin.stop_loss_count += 1  
    
                    ######################################################
                    ### 【4】UPDATE PARTIALLY CLOSED ORDER
                    ######################################################                
    
                    if sub_order_incomplete:
                        ### add partially closed sell order child
                        self.update_partially_closed_order()
                        self.gvar.callers_method_name = 'stop_loss(0):'    
                   
                    ######################################################
                    ### 【5】CANCEL CLOSED BUY ORDER SUB
                    ######################################################
    
                    ### cancel closed buy order sub  
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}CANCEL{Bcolors.ENDC} closed buy order sub({sub_buy_order_id}) ") 
    
                    # increment buy cancel count
                    self.gvar.buy_cancel_count += 1   # cancel closed buy order (with sell order)            
                    self.coin.buy_cancel_count += 1   # cancel closed buy order (with sell order)
    
                    ### set a cancelled flag for buy order sub: PK(buy_time + buy_order_id) ★
                    find_mask = (buy_sub_df['buy_time'] == sub_buy_time) & (buy_sub_df['buy_order_id'] == sub_buy_order_id)
                    buy_sub_df.loc[find_mask, 'cancelled'] = True   
    
                    ######################################################
                    ### 【6】CANCEL OPEN/INCOMPLETE SELL ORDER SUB
                    ######################################################
    
                    ### for open or incomplete sell sub order     
    
                    # cancel open/incomplete sell order sub                                                    
                    status, msg_code, msg_str = self.api.exe_cancel_order(sub_sell_order_id)    # ★  
    
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}CANCEL{Bcolors.ENDC} open/incomplete sell order sub({sub_sell_order_id}): exe_cancel_order() ▶ {status=} ") 
    
                    sleep(0.5)    # sleep 0.5 sec
    
                    ###########################################################################################
                    ### 【7】RESELL OUTSTANDING SELL ORDER SUB QTY : EXECUTE SELL CLOSE ORDER MARKET (FAK)
                    ###########################################################################################
    
                    ### resell open/incomplete sell order sub with market price (stop loss price)
    
                    outstanding_qty = sub_qty - sub_real_qty                
                    new_sell_qty = outstanding_qty                                
                    qty = new_sell_qty
    
                    if sub_real_price > 0:
                        new_sell_price = sub_real_price + sub_real_fee                                                                                     
                    else:                    
                        new_sell_price = sub_price                                                                  
    
                    ### get the latest close price from the GMO master
    
                    # get a column position of the GMO master              
                    mas_latest_close_price = mas_df.iloc[-1]['close']   # get latest close price (GMO)              
          
                    status = 0            
                    loop_count = 0 
    
                    while True:
                        loop_count += 1    
    
                        ### resell sell order sub outstanding qty
                        status, res_df, msg_code, msg_str = self.api.exe_sell_close_order_market_wait(sub_position_id, qty, new_sell_price, mas_latest_close_price)  # ★ FAK(Fill and Kill): new_sell_price, mas_latest_close_price are used for fake mode ; 4BTC; 1BTC 
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}RESELL({loop_count}){Bcolors.ENDC} open/incomplete sell order sub({sub_sell_order_id}): exe_sell_close_order_market_wait() ▶ {status=} ") 
            
                        # error ?
                        if status != 0:
                            if status == 1 and msg_code == 'ERR-208':   # Exceeds the available balance
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC}{Bcolors.FAIL} the available balance exceeded error... ERR-208{Bcolors.ENDC} ")
                                self.debug.sound_alert(1)  
                                break   # exit while true loop and continue to read next sell order sub        
                            elif status == 8 and msg_code == 'ERR-888': # get_price() time out error ?
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} exe_sell_close_order_market_wait() time out error: ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                                break   # exit while true loop and continue to read next sell order sub     
                            else:
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} exe_sell_close_order_market_wait() ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                                break   # exit while tue loop and continue to read next sell order sub  
                            # end of if status == 1 and msg_code == 'ERR-201':
                        # end of if status != 0:        
    
                        ### no error : res_df.shape[0] >= 1 (single or multiple rows)   
    
                        ######################################################################################################
                        ### 【8】ADD SELL ORDER CHILD ★ DO NOT CREATE RESELL VERSION OF NEW SELL ORDER SUB ★ 
                        ######################################################################################################
    
                        # res_df = get_price() response for exe_sell_order_market_wait()
                        # ----------------------------------------------------------------
                        #  #   Column       Dtype         
                        # ----------------------------------------------------------------         
                        #  0   executionId  int64         
                        #  1   fee          float64       
                        #  2   lossGain     float64                               
                        #  3   orderId      object(string)   
                        #  4   posiitonId   object(string)                          
                        #  5   price        float64  
                        #  6   settleType   object('OPEN' or 'CLOSE')  
                        #  7   side         object('BUY' or 'SELL')                  
                        #  8   size         float64           
                        #  9   symbol       object('BTC')                        
                        # 10   timestamp    datetime64[ns]     
                        # ----------------------------------------------------------------                   
    
                        ### IMPORTANT ★ DO NOT CREATE RESELL VERSION OF NEW SELL ORDER SUB ★ 
    
                        ### append a new record into the sell order child (★ this is the resell version of buy order ★)
    
                        # define sell order child list
                        order_id_list = []
                        position_id_list = []
                        real_qty_list = []
                        real_price_list = []
                        real_fee_list = []  
    
                        # response df for get_price for exe_sell_close_order_market() 
                        for k in range(len(res_df)):
                            order_id    = res_df.loc[k, 'orderId']          # orderId (same orderId): resell version of orderId
                            position_id = res_df.loc[k, 'positionId']       # positionId (same positionId)
                            real_qty    = res_df.loc[k, 'size']             # real qty : 
                            real_price  = res_df.loc[k, 'price']            # real price
                            real_fee    = res_df.loc[k, 'fee']              # real fee        
    
                            order_id_list.append(order_id)                  # S9999-999999-999999
                            position_id_list.append(position_id)            # P9999-999999-999999
                            real_qty_list.append(real_qty)
                            real_price_list.append(real_price)
                            real_fee_list.append(real_fee)
    
                            ### write a sell order child (resell version) ★              
                            # write_sell_order_child(sell_time: utcnow, sell_order_id: str, buy_time: utcnow, buy_order_id: str, position_id: str, qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None:             
                            _qty_ = real_qty; _price_ = real_price
                            self.csv.write_sell_order_child(sell_time, order_id, buy_time, sub_buy_order_id, position_id, _qty_, _price_, real_qty, real_price, real_fee)    
                        # end of for k in range(len(res_df)):              
      
                        sum_real_qty = round(sum(real_qty_list), _q_)   # round(999.12, _q_)
                        sum_real_fee = round(sum(real_fee_list), 0)     # zero(0)          
    
                        if sum(real_price_list) > 0:
                            mean_real_price = round(statistics.mean(real_price_list), _p_) #  round(999999.123, _p_)
                        else:
                            mean_real_price = 0                       
    
                        sub_real_qty_list.append(sum_real_qty)                
                        sub_real_price_list.append(mean_real_price)
                        sub_real_fee_list.append(sum_real_fee)
    
                        ### check exit condition:      
    
                        sub_sum_real_qty = round(sum(sub_real_qty_list), _q_)     # round(9999.12, _q_)     
                        new_sub_sum_real_qty = sub_real_qty + sub_sum_real_qty    
                         
                        # ####################################################### TEMP TEMP TEMP
                        # temp_qty = round(sub_qty, _q_)
                        # temp_real_qty = round(new_sub_sum_real_qty, _q_)
                        # self.debug.trace_warn(f".... DEBUG: {temp_qty=:,.{_q_}f}, {temp_real_qty=:,.{_q_}f} ")
                        # ####################################################### TEMP TEMP TEMP  
    
                        if round(sub_qty, _q_) == round(new_sub_sum_real_qty, _q_):                                               
                            # increment sell cancel count
                            self.gvar.sell_cancel_count += 1  # cancel open/incomplete sell order            
                            self.coin.sell_cancel_count += 1  # cancel open/incomplete sell order
                            break   # exit while true loop 
                        else:       # outstanding buy_qty exists                    
                            pass    # skip (do nothing)                   
                        # end of round(sub_qty, digits) == round(new_sub_sum_real_qty, digits):
    
                        ### partial exe_sell_order_market_wait() = continue to sell remaining qty                        
                        remaining_qty = new_sell_qty - new_sub_sum_real_qty   
                        qty = remaining_qty
    
                        # continue exe_sell_close_order_market_wait() ★                                                             
                    # end of while True:                            
                      
                    #################################################################################
                    ### 【9】UPDATE SELL ORDER SUB (real_qty, real_price, real_fee, stop_loss)
                    #################################################################################
    
                    sub_sum_real_qty = round(sum(sub_real_qty_list), _q_)   # round(999.12,_q_)                                
                    sub_sum_real_fee = round(sum(sub_real_fee_list), 0)     # zero(0)
    
                    if sum(sub_real_price_list) > 0:
                        sub_mean_real_price = round(statistics.mean(sub_real_price_list), _p_)  # round(999999.123, _p_)
                    else:
                        sub_mean_real_price = 0                   
    
                    header_real_qty_list.append(sub_sum_real_qty)
                    header_real_price_list.append(sub_mean_real_price)
                    header_real_fee_list.append(sub_sum_real_fee)
    
                    ### update the sell order sub (real_qty, real_price, real_fee, stop_loss) ★
                    find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)
                    dfx = sell_sub_df[find_mask]
                    # not found sell order sub ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL} sell order sub not found {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order sub => update
                        sell_sub_df.loc[find_mask, 'real_qty'] += sub_sum_real_qty      # add real_qty (resell version)                           
                        sell_sub_df.loc[find_mask, 'real_price'] = sub_mean_real_price  # update real_price (resell version)
                        sell_sub_df.loc[find_mask, 'real_fee'] += sub_sum_real_fee      # add real_fee (resell version)
                        sell_sub_df.loc[find_mask, 'stop_loss'] = True
                        ix = dfx.index[0]
                        new_sub_sum_real_qty = sell_sub_df.loc[ix, 'real_qty']
                        new_sub_sum_real_fee = sell_sub_df.loc[ix, 'real_fee']                    
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub(sell_order_id={sub_sell_order_id}, real_qty={new_sub_sum_real_qty:,.{_q_}f}, real_price={sub_mean_real_price:,.{_p_}f}, real_fee={new_sub_sum_real_fee:,.0f}, stop_loss=True) ")                     
                    # end of if dfx.shape[0] == 0:  
    
                    #################################################################################
                    ### 【10】UPDATE ORDER CSV FILE (stop_loss, order_closed)
                    #################################################################################
    
                    ### update order info (stop_loss=True, order_closed) ★        
                    # update_order_csv_buy(buy_time: utcnow, buy_order_id: str, col_name_list: list, col_value_list: list) -> None: # PK(buy_time + buy_order_id)
                    self.csv.update_order_csv_buy(sub_buy_time, sub_buy_order_id, col_name_list=['stop_loss'], col_value_list=[True])                                                                               
    
                    # reset sell order sub list values
                    sub_real_qty_list = []
                    sub_real_price_list = []
                    sub_real_fee_list = [] 
    
                    # continue to next sell order sub... ★ 
                # end of for j in range(len(sell_sub_df)):
    
                #################################################################################
                ### 【11】SAVE BUY/SELL ORDER SUB TO CSV FILES
                #################################################################################
    
                ### CLOSE (save the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
                csv_file = self.folder_trading_type + f'buy_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/buy_order_sub(BTC_JPY).csv  
                buy_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode                
                            
                ### CLOSE (save the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
                csv_file = self.folder_trading_type + f'sell_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/sell_order_sub(BTC_JPY).csv  
                sell_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode                                              
    
                #################################################################################
                ### 【12】UPDATE SELL ORDER (real_qty, real_price, real_fee)
                #################################################################################
    
                if len(header_real_qty_list) > 0:
                    ### update sell order (real_qty, real_price, real_fee) ★
                    header_sum_real_qty = round(sum(header_real_qty_list), _q_)     # round(999.12, _q_        
                    header_sum_real_fee = round(sum(header_real_fee_list), 0)       # zero(0)            
                    
                    if sum(header_real_price_list) > 0:
                        header_mean_real_price = round(statistics.mean(header_real_price_list), _p_)    # round(999999.123, _p_)
                    else:
                        header_mean_real_price = 0            
    
                    find_mask = sell_df['sell_time'] == sell_time
                    dfx = sell_df[find_mask]
                    # not found sell order ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order({sell_time:%Y-%m-%d %H:%M:%S}) ▶ {Bcolors.FAIL} order({sell_time=:%Y-%m-%d %H:%M:%S}) not found {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order header => update
                        sell_df.loc[find_mask, 'real_qty'] += header_sum_real_qty       # add real_qty (resell version)                          
                        sell_df.loc[find_mask, 'real_price'] = header_mean_real_price   # update real_price (resell version) 
                        sell_df.loc[find_mask, 'real_fee'] += header_sum_real_fee       # add real_fee (resell version)
                        ix = dfx.index[0]
                        new_header_sum_real_qty = sell_df.loc[ix, 'real_qty'] 
                        new_header_sum_real_fee = sell_df.loc[ix, 'real_fee']
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={new_header_sum_real_qty:,.{_q_}f}, real_price={header_mean_real_price:,.{_p_}f}, real_fee={new_header_sum_real_fee:,.0f}) ") 
                    # end of if dfx.shape[0] == 0:                   
                # end of if len(header_real_qty_list) > 0:
    
                # reset list values
                header_real_qty_list = []
                header_real_price_list = []
                header_real_fee_list = []     
    
                # continue to next sell order 
            # end of for i in range(len(sell_df)):
    
            #################################################################################
            ### 【13】SAVE BUY/SELL ORDER TO CSV FILES 
            #################################################################################
    
            ### CLOSE (save the buy order csv file) : buy_order(BTC_JPY).csv         
            csv_file = self.folder_trading_type + f'buy_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/buy_order(BTC_JPY).csv  
            buy_df.to_csv(csv_file, header=False, index=False) # OverWrite mode  
    
            ### CLOSE (save the sell order csv file) : sell_order(BTC_JPY).csv      
            csv_file = self.folder_trading_type + f'sell_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/sell_order(BTC_JPY).csv   
            sell_df.to_csv(csv_file, header=False, index=False) # OverWrite mode        
    
            return
    
        ##############################     
        def stop_loss_1(self) -> None:  
            """
    
            """
               
            return
    
        ########################################    
        def cancel_pending_order(self) -> None: 
            """        
    
            """         
    
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal
            self.gvar.callers_method_name = 'cancel_pending_order():'
            self.debug.trace_write(f".... {self.gvar.callers_method_name} ")  
    
            df = self.coin.master2_df              
            mas_df = pd.DataFrame(df)   # cast df (master dataframe)
          
            ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)  
              
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
    
            #################################################
            ### 【1】READ ALL SELL ORDERs 
            #################################################
    
            # define sell order list
            header_real_qty_list = []
            header_real_price_list = []
            header_real_fee_list = []        
    
            ### get all sell order 
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']   # ★ unique time                 
                buy_time        = sell_df.loc[i, 'buy_time']    # ★ unique time                       
                sell_qty        = sell_df.loc[i, 'qty']
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True         
            
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell order sub
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell order sub ?
                    if dfx.shape[0] > 0:
                        ix = dfx.index[0]                
                        sell_order_id = dfx.loc[ix, 'sell_order_id']    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake order sub({sell_order_id})... ")
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_order_id}) has been closed: {sell_order_closed=} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_time:%Y-%m-%d %H:%M:%S}) has been closed: {sell_order_closed=} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # skip closed order    
                # end of if sell_order_closed:  
    
                #################################################################
                ### 【2】READ ALL SELL ORDER SUB (OPEN OR INCOMPLETE STATUS)
                #################################################################
    
                ### for sell order sub (open or incomplete status)
    
                # define sell order sub list
                sub_real_qty_list = []
                sub_real_price_list = []
                sub_real_fee_list = [] 
    
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order
                for j in range(len(sell_subx_df)):   # ★ sell_subx_df ★
                    ### get sell order sub info
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # ★ 'S9999-999999-999999' (unique id)           
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # ★ 'B9999-999999-999999' (non unique id) : sell sub(many) => buy_sub(one)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # ★ (unique id)
                    sub_qty             = sell_subx_df.loc[j, 'qty']                                  
                    sub_price           = sell_subx_df.loc[j, 'price']               
                    sub_real_qty        = sell_subx_df.loc[j, 'real_qty']                        
                    sub_real_price      = sell_subx_df.loc[j, 'real_price']            
                    sub_real_fee        = sell_subx_df.loc[j, 'real_fee']                  
                    sub_stop_loss       = sell_subx_df.loc[j, 'stop_loss']
                    sub_cancelled       = sell_subx_df.loc[j, 'cancelled']                
            
                    sub_order_open = False
                    sub_order_closed = False
                    sub_order_incomplete = False
    
                    if sub_real_qty == 0:
                        sub_order_open = True
                    elif sub_qty == sub_real_qty:
                        sub_order_closed = True
                    else:
                        sub_order_incomplete = True                             
    
                    # fake sell order sub ? => SKIP    
                    if sub_sell_order_id.startswith('Fake') :
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id}) ") 
                        continue               
    
                    # sell order sub has been stopped loss ? => SKIP
                    if sub_stop_loss: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss ▶ {sub_stop_loss=} ")  
                        continue
    
                    # cancelled sell order sub ? => SKIP    
                    if sub_cancelled: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled: {sub_cancelled=} ") 
                        continue           
    
                    # closed sell order sub ? => SKIP    
                    if sub_order_closed: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed: {sub_order_closed=} ") 
                        continue  
    
                    ### open or incomplete sell order sub    
    
                    ######################################################
                    ### 【3】UPDATE PARTIALLY CLOSED ORDER
                    ######################################################                
    
                    if sub_order_incomplete:
                        ### add partially closed sell order child
                        self.update_partially_closed_order()
                        self.gvar.callers_method_name = 'cancel_pending_order():'                
    
                    ##########################################################################################
                    ### 【4】CANCEL CLOSED BUY ORDER SUB : ★ Sell Order Sub(many) => Buy Order Sub(one) ★
                    ##########################################################################################
    
                    ### for closed buy order sub                             
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}CANCEL{Bcolors.ENDC} closed buy order sub({sub_buy_order_id}) ") 
    
                    # increment buy cancel count
                    self.gvar.buy_cancel_count += 1   # cancel closed buy order             
                    self.coin.buy_cancel_count += 1   # cancel closed buy order 
    
                    ### set a cancelled flag for buy order sub : ★ PK(buy_time + buy_order_id) ★
                    find_mask = (buy_sub_df['buy_time'] == sub_buy_time) & (buy_sub_df['buy_order_id'] == sub_buy_order_id)
                    buy_sub_df.loc[find_mask, 'cancelled'] = True                
                    self.debug.trace_write(f".... {self.gvar.callers_method_name} update_buy_order_sub(buy_order_id={sub_buy_order_id}, cancelled=True) ")                                                              
                                       
                    ######################################################
                    ### 【5】CANCEL OPEN/INCOMPLETE SELL ORDER SUB
                    ######################################################
    
                    ### cancel open or incomplete sell order sub  
    
                    ### cancel sell order sub                    
                    status, msg_code, msg_str = self.api.exe_cancel_order(sub_sell_order_id)    # ★        
                    self.debug.print_log(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}CANCEL{Bcolors.ENDC} open/incomplete sell order sub({sub_sell_order_id}): exe_cancel_order() ▶ {status=} ")   
                    sleep(0.5)  # sleep 0.5 sec                       
    
                    ##################################################################################################
                    ### 【6】RESELL OUTSTANDING SELL ORDER SUB QTY : EXECUTE SELL CLOSE ORDER MARKET WAIT (FAK)
                    ##################################################################################################
    
                    ### resell open or incomplete sell order sub with market price 
    
                    outstanding_qty = sub_qty - sub_real_qty    # calculate outstanding qty 
                    new_sell_qty = outstanding_qty                                  
                    qty = new_sell_qty   
    
                    if sub_real_price > 0:
                        new_sell_price = sub_real_price + sub_real_fee                              
                    else:
                        new_sell_price = sub_price
    
                    ### get the latest close price from the GMO master
    
                    # get a column position of the GMO master             
                    mas_latest_close_price = mas_df.iloc[-1]['close']                                 # get latest close price (GMO)                         
    
                    status = 0
                    loop_count = 0
    
                    while True:
                        loop_count += 1    
       
                        # exe_sell_close_order_market_wait(position_id: str, qty: float, price: float, latest_close_price=0.0) -> tuple:     #  return (status, df, msg_code, msg_str) 
                        status, res_df, msg_code, msg_str = self.api.exe_sell_close_order_market_wait(sub_position_id, qty, new_sell_price, mas_latest_close_price)  # ★ FAK(Fill and Kill): new_sell_price_str, mas_latest_close_price_str ares used for fake mode ; 4BTC; 1BTC               
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}RESELL({loop_count}){Bcolors.ENDC} open/incomplete sell order sub({sub_sell_order_id}): exe_sell_close_order_market_wait() ▶ {status=} ") 
            
                        # error ?
                        if status != 0:
                            if status == 1 and msg_code == 'ERR-208':   # Exceeds the available balance
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC}{Bcolors.FAIL} the available balance exceeded error... ERR-208{Bcolors.ENDC} ")
                                self.debug.sound_alert(1)  
                                break   # exit while true loop and continue to read next sell sub orders                                  
                            elif status == 8 and msg_code == 'ERR-888': # get_price() time out error ?
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} exe_sell_close_order_market_wait() time out error: ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                                break   # exit while true loop and continue to read next sell sub orders 
                            else: # misc error
                                self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} exe_sell_close_order_market_wait() misc error ▶ {Bcolors.FAIL} {status=}, {msg_code} - {msg_str}  {Bcolors.ENDC} ")
                                break   # exit while true loop and continue to read next sell order sub                    
                            # end of if status == 1 and msg_code == 'ERR-201':
                        # end of if status != 0:        
    
                        ### no error : res_df.shape[0] >= 1 (single or multiple rows)
    
                        ##################################################################################################
                        ### 【7】ADD SELL ORDER CHILD ★ DO NOT CREATE RESELL VERSION OF NEW SELL ORDER SUB ★ 
                        ##################################################################################################
    
                        # res_df = response for exe_sell_close_order_market_wait() 
                        # -----------------------------------------------------------
                        #  #   Column       Dtype         
                        # -----------------------------------------------------------         
                        #  0   executionId  int64         
                        #  1   fee          float64       
                        #  2   lossGain     float64                               
                        #  3   orderId      object(string)  (non unique)  
                        #  4   positionId   object(string)  (non unique)     
                        #  5   price        float64  
                        #  6   settleType   object('OPEN' or 'CLOSE')  
                        #  7   side         object('BUY' or 'SELL')                  
                        #  8   size         float64           
                        #  9   symbol       object('BTC_JPY')                        
                        # 10   timestamp    datetime64[ns]  
                        # ----------------------------------------------------------
    
                        ### IMPORTANT ★ DO NOT CREATE RESELL VERSION OF NEW SELL ORDER SUB ★ 
                        
                        ### append a new record into the sell order child  (★ this is the resell version of sell order child ★)
    
                        # define sell order child list
                        order_id_list = []
                        position_id_list = []
                        real_qty_list = []
                        real_price_list = []
                        real_fee_list = []   
    
                        # response for exe_sell_close_order_market_wait()
                        for k in range(len(res_df)):
                            order_id    = res_df.loc[k, 'orderId']          # orderId (same orderId) : resell version of orderId         
                            position_id = res_df.loc[k, 'positionId']       # positionId (same positionId)               
                            real_qty    = res_df.loc[k, 'size']             # real qty     
                            real_price  = res_df.loc[k, 'price']            # real price
                            real_fee    = res_df.loc[k, 'fee']              # real fee
    
                            order_id_list.append(order_id)                  # S9999-999999-999999
                            position_id_list.append(position_id)            # P9999-999999-999999
                            real_qty_list.append(real_qty)                  # 
                            real_price_list.append(real_price)
                            real_fee_list.append(real_fee)
    
                            ### write a sell order child (resell version) ★   
                            # write_sell_order_child(sell_time: utcnow, sell_order_id: str, buy_time: utcnow, buy_order_id: str, position_id: str, qty=0.0, price=0.0, real_qty=0.0, real_price=0.0, real_fee=0.0) -> None: 
                            _qty_ = real_qty; _price_ = real_price
                            self.csv.write_sell_order_child(sell_time, order_id, buy_time, sub_buy_order_id, position_id, _qty_, _price_, real_qty, real_price, real_fee)                             
                        # end of for k in range(len(res_df)):                      
    
                        sum_real_qty = round(sum(real_qty_list), _q_)                           # round(999.12, _q_)                                 
                        sum_real_fee = round(sum(real_fee_list), 0)                             # zero(0)
    
                        if sum(real_price_list) > 0:
                            mean_real_price = round(statistics.mean(real_price_list), _p_)      # round(999999.123, _p_)
                        else:
                            mean_real_price = 0    
    
                        sub_real_qty_list.append(sum_real_qty)                                  
                        sub_real_price_list.append(mean_real_price)
                        sub_real_fee_list.append(sum_real_fee)
                                          
                        ### check exit condition:      
                         
                        sub_sum_real_qty = round(sum(sub_real_qty_list), _q_)                             
                        new_sub_sum_real_qty = sub_real_qty + sub_sum_real_qty                  
                
                        # no outstanding sell_qty ?
                        if round(sub_qty, _q_) == round(new_sub_sum_real_qty, _q_):            
                            # increment sell cancel count
                            self.gvar.sell_cancel_count += 1  # cancel open/incomplete sell order            
                            self.coin.sell_cancel_count += 1  # cancel open/incomplete sell order
                            break   # exit while true loop
                        else:       # outstanding sell_qty exists
                            pass    # skip (do nothing)                 
                        # end of if round(sub_qty, digits) == round(new_sub_sum_real_qty, digits):
    
                        ### partial exe_sell_close_order_market_wait() = continue to sell remaining qty
                            
                        remaining_qty = new_sell_qty - new_sub_sum_real_qty    
                        qty = remaining_qty
    
                        # continue exe_sell_close_order_market_wait() ★
                    # end of while True:   
                     
                    #################################################################################
                    ### 【8】UPDATE SELL ORDER SUB (real_qty, real_price, real_fee, cancelled)
                    #################################################################################                                  
        
                    sub_sum_real_qty = round(sum(sub_real_qty_list), _q_)                                                     
                    sub_sum_real_fee = round(sum(sub_real_fee_list), 0)                    
    
                    if sum(sub_real_price_list) > 0:
                        sub_mean_real_price = round(statistics.mean(sub_real_price_list), _p_) 
                    else:
                        sub_mean_real_price = 0                      
    
                    header_real_qty_list.append(sub_sum_real_qty)                          
                    header_real_price_list.append(sub_mean_real_price)
                    header_real_fee_list.append(sub_sum_real_fee)
    
                    ### update the sell order sub (real_qty, real_price, real_fee, cancelled) 
                    find_mask = (sell_sub_df['sell_time'] == sub_sell_time) & (sell_sub_df['sell_order_id'] == sub_sell_order_id)
                    dfx = sell_sub_df[find_mask]
                    # not found sell order sub ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order_sub({sub_sell_order_id}) ▶ {Bcolors.FAIL}sell order sub not found {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order sub => update 
                        sell_sub_df.loc[find_mask, 'real_qty'] += sub_sum_real_qty      # add real_qty (resell version)                                  
                        sell_sub_df.loc[find_mask, 'real_price'] = sub_mean_real_price  # update real_price (resell version)
                        sell_sub_df.loc[find_mask, 'real_fee'] += sub_sum_real_fee      # add real_fee (resell version)
                        sell_sub_df.loc[find_mask, 'cancelled'] = True
                        ix = dfx.index[0]
                        new_sub_sum_real_qty = sell_sub_df.loc[ix, 'real_qty']         
                        new_sub_sum_real_fee = sell_sub_df.loc[ix, 'real_fee']
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order_sub(sell_order_id={sub_sell_order_id}, real_qty={new_sub_sum_real_qty:,.{_q_}f}, real_price={sub_mean_real_price:,.{_p_}f}, real_fee={new_sub_sum_real_fee:,.0f}, cancelled=True) ")                                    
                    # end of if dfx.shape[0] == 0:   
                        
                    #################################################################################
                    ### 【9】UPDATE ORDER CSV FILE (cancelled, order_closed)
                    #################################################################################                        
                        
                    ### update order csv file (cancelled=True, order_closed=True)  ★  
                    # update_order_info_buy(buy_time, buy_order_id, symbol='BTC_JPY', col_name=any, col_value=any):   
                    # update_order_csv_buy(buy_time: utcnow, buy_order_id: str, col_name_list: list, col_value_list: list) -> None: # PK(buy_time + buy_order_id)
                    self.csv.update_order_csv_buy(sub_buy_time, sub_buy_order_id, col_name_list=['cancelled'], col_value_list=[True]) # PK(buy_time + buy_order_id)                                                                                                                       
    
                    # reset sell order sub list values
                    sub_real_qty_list = []
                    sub_real_price_list = []
                    sub_real_fee_list = [] 
    
                    # continue to next sell order sub
                # end of for j in range(len(sell_subx_df)):     
    
                #################################################################################
                ### 【10】SAVE BUY/SELL ORDER SUB TO CSV FILES
                #################################################################################
    
                ### CLOSE (save the buy order sub csv file)
                csv_file = self.folder_trading_type + f'buy_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/buy_order_sub(BTC_JPY).csv  
                buy_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode   
                                         
                ### CLOSE (save the sell order sub csv file) 
                csv_file = self.folder_trading_type + f'sell_order_sub({symbol}).csv' 
                # desktop-pc/bot/buy_sell/sell_order_sub(BTC_JPY).csv  
                sell_sub_df.to_csv(csv_file, header=False, index=False) # OverWrite mode                                             
    
                #################################################################################
                ### 【11】UPDATE SELL ORDER (real_qty, real_price, real_fee)
                #################################################################################
                    
                if len(header_real_qty_list) > 0:    
                    ### update sell order (real_qty, real_price, real_fee) ★
                    header_sum_real_qty = round(sum(header_real_qty_list), _q_)                      
                    header_sum_real_fee = round(sum(header_real_fee_list), 0)                            
                    
                    if sum(header_real_price_list) > 0:
                        header_mean_real_price = round(statistics.mean(header_real_price_list), _p_)  
                    else:
                        header_mean_real_price = 0    
    
                    find_mask = sell_df['sell_time'] == sell_time
                    dfx = sell_df[find_mask]
                    # not found sell order ?
                    if dfx.shape[0] == 0:
                        self.debug.print_log(f".... {Bcolors.WARNING}{self.gvar.callers_method_name}{Bcolors.ENDC} update_sell_order() ▶ {Bcolors.FAIL}sell order({sell_time=:%Y-%m-%d %H:%M:%S}) not found {dfx.shape=}{Bcolors.ENDC} ")   
                    else: # found sell order => update
                        sell_df.loc[find_mask, 'real_qty'] += header_sum_real_qty     # add real_qty (resell version)                            
                        sell_df.loc[find_mask, 'real_price'] = header_mean_real_price # update real_price (resell version) 
                        sell_df.loc[find_mask, 'real_fee'] += header_sum_real_fee     # add real_fee (resell version) 
                        ix = dfx.index[0]
                        new_header_sum_real_qty = sell_df.loc[ix, 'real_qty'] 
                        new_header_sum_real_fee = sell_df.loc[ix, 'real_fee']                
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} update_sell_order({sell_time=:%Y-%m-%d %H:%M:%S}, real_qty={new_header_sum_real_qty:,.{_q_}f}, real_price={header_mean_real_price:,.{_p_}f}, real_fee={new_header_sum_real_fee:,.0f}) ")                                                    
                    # end of if dfx.shape[0] == 0:          
                # end of if len(header_real_qty_list) > 0:
    
                # reset sell header list values
                header_real_qty_list = []
                header_real_price_list = []
                header_real_fee_list = []     
    
                # continue to next sell order... ★              
            # end of for i in range(len(sell_df)):  
    
            #################################################################################
            ### 【12】SAVE BUY/SELL ORDER TO CSV FILES 
            #################################################################################        
    
            ### CLOSE (save the buy order csv file)          
            csv_file = self.folder_trading_type + f'buy_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/buy_order(BTC_JPY).csv  
            buy_df.to_csv(csv_file, header=False, index=False) # OverWrite mode  
    
            ### CLOSE (save the sell order csv file)       
            csv_file = self.folder_trading_type + f'sell_order({symbol}).csv' 
            # desktop-pc/bot/buy_sell/sell_order(BTC_JPY).csv   
            sell_df.to_csv(csv_file, header=False, index=False) # OverWrite mode 
    
            return
    
        ####################################### 
        def close_pending_order(self) -> None:
            """        
    
            """        
    
            symbol = self.coin.symbol; _q_ = self.coin.qty_decimal; _p_ = self.coin.price_decimal
            self.gvar.callers_method_name = 'close_pending_order():'
            self.debug.trace_write(f".... {self.gvar.callers_method_name} ")  
    
            # df = self.coin.master2_df      
            # mas_df = pd.DataFrame(df)   # cast df (master dataframe)
    
            ### OPEN (load the order csv file) : order(BTC_JPY).csv 
            ord_df = self.csv.get_order_csv()          
          
            # ### OPEN (load the buy order csv file) : buy_order(BTC_JPY).csv 
            # buy_df = self.csv.get_buy_order() # PK(buy_time)        
    
            # ### OPEN (load the buy order sub csv file) : buy_order_sub(BTC_JPY).csv 
            # buy_sub_df = self.csv.get_buy_order_sub() # PK(buy_time + order_id)  
    
            ### OPEN (load the buy order child log csv file) : buy_order_child(BTC_JPY).csv 
            buy_ch_df = self.csv.get_buy_order_child() # PK(buy_time + buy_order_id + position_id)         
              
            ### OPEN (load the sell order csv file) : sell_order(BTC_JPY).csv 
            sell_df = self.csv.get_sell_order() # PK(sell_time)        
    
            ### OPEN (load the sell order sub csv file) : sell_order_sub(BTC_JPY).csv 
            sell_sub_df = self.csv.get_sell_order_sub() # PK(sell_time + order_id)   
    
            # ### OPEN (load the sell order child csv file) : sell_order_child(BTC_JPY).csv 
            # sell_ch_df = self.csv.get_sell_order_child() # PK(sell_time + sell_order_id + position_id)          
    
            #################################################
            ### 【1】READ ALL SELL ORDER 
            #################################################
         
            ### get all sell orders 
            for i in range(len(sell_df)):
                ### get sell order info
                sell_time       = sell_df.loc[i, 'sell_time']   # ★ unique time                 
                buy_time        = sell_df.loc[i, 'buy_time']    # ★ unique time                       
                sell_qty        = sell_df.loc[i, 'qty']
                sell_price      = sell_df.loc[i, 'price']
                sell_real_qty   = sell_df.loc[i, 'real_qty']
                sell_real_price = sell_df.loc[i, 'real_price']
                sell_real_fee   = sell_df.loc[i, 'real_fee']
    
                sell_order_open = False
                sell_order_closed = False
                sell_order_incomplete = False
    
                if sell_real_qty == 0:
                    sell_order_open = True
                elif sell_qty == sell_real_qty:
                    sell_order_closed = True
                else:
                    sell_order_incomplete = True         
            
                # closed sell order ? => SKIP    
                if sell_order_closed: 
                    # find the sell sub order log
                    find_mask = sell_sub_df['sell_time'] == sell_time
                    dfx = sell_sub_df[find_mask]
                    # found the sell sub order ?
                    if dfx.shape[0] > 0:
                        ix = dfx.index[0]                
                        sell_order_id = dfx.loc[ix, 'sell_order_id']    
                        if sell_order_id.startswith('Fake'):
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sell_order_id})... ")
                        else:
                            pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_order_id}) has been closed: {sell_order_closed=} ") 
                    else:
                       pass # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order({sell_time=:%Y-%m-%d %H:%M:%S}) has been closed: {sell_order_closed=} ") 
                    # end of if dfx.shape[0] > 0:
                    continue    # skip closed sell order    
                # end of if sell_order_closed:  
    
                #################################################################
                ### 【2】READ ALL SELL ORDER SUB (OPEN OR INCOMPLETE STATUS)
                #################################################################
    
                ### for sell order sub (open or incomplete status)
    
                # filter the sell order sub by sell_time
                filter_mask = sell_sub_df['sell_time'] == sell_time
                sell_subx_df = sell_sub_df[filter_mask]
                if sell_subx_df.shape[0] > 0:
                    sell_subx_df.reset_index(inplace=True)
    
                # get all sell order sub for this sell order
                for j in range(len(sell_subx_df)):   # ★ sell_subx_df ★
                    ### get sell order sub info
                    sub_sell_time       = sell_subx_df.loc[j, 'sell_time']          # non unique time
                    sub_buy_time        = sell_subx_df.loc[j, 'buy_time']           # non unique time
                    sub_sell_order_id   = sell_subx_df.loc[j, 'sell_order_id']      # ★ 'S9999-999999-999999' (unique id)           
                    sub_buy_order_id    = sell_subx_df.loc[j, 'buy_order_id']       # ★ 'B9999-999999-999999' (non unique id) : sell sub(many) => buy_sub(one)
                    sub_position_id     = sell_subx_df.loc[j, 'position_id']        # ★ (unique id)
                    sub_qty             = sell_subx_df.loc[j, 'qty']                                  
                    sub_price           = sell_subx_df.loc[j, 'price']               
                    sub_real_qty        = sell_subx_df.loc[j, 'real_qty']                        
                    sub_real_price      = sell_subx_df.loc[j, 'real_price']            
                    sub_real_fee        = sell_subx_df.loc[j, 'real_fee']  
                    sub_get_price_count = sell_subx_df.loc[j, 'get_price_count']                
                    sub_stop_loss       = sell_subx_df.loc[j, 'stop_loss']
                    sub_cancelled       = sell_subx_df.loc[j, 'cancelled']                
            
                    sub_order_open = False
                    sub_order_closed = False
                    sub_order_incomplete = False
    
                    if sub_real_qty == 0:
                        sub_order_open = True
                    elif sub_qty == sub_real_qty:
                        sub_order_closed = True
                    else:
                        sub_order_incomplete = True                             
    
                    # fake sell order sub ? => SKIP    
                    if sub_sell_order_id.startswith('Fake') :
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} fake sell order sub({sub_sell_order_id}) ") 
                        continue               
    
                    # sell order sub has been stopped loss ? => SKIP
                    if sub_stop_loss: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been stopped loss ▶ {sub_stop_loss=} ")  
                        continue
    
                    # cancelled sell order sub ? => SKIP    
                    if sub_cancelled: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been cancelled: {sub_cancelled=} ") 
                        continue           
    
                    # closed sell order sub ? => SKIP    
                    if sub_order_closed: 
                        # self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.OKCYAN}SKIP{Bcolors.ENDC} sell order sub({sub_sell_order_id}) has been closed: {sub_order_closed=} ") 
                        continue  
    
                    ### open or incomplete sell order sub
                     
                    #################################################################
                    ### 【3】CALCULATE LEVERAGE FEE AND UPDATE ORDER CSV FILE
                    #################################################################
    
                    ### calculate leverage fee 
    
                    # get buy order child 
    
                    find_mask = (buy_ch_df['buy_time'] == sub_buy_time) & (buy_ch_df['buy_order_id'] == sub_buy_order_id) & (buy_ch_df['position_id'] == sub_position_id) 
                    dfx = buy_ch_df[find_mask]
                    # not found buy order child ?
                    if dfx.shape[0] == 0:
                        self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}not found buy order child {Bcolors.ENDC} : {sub_buy_time=:%Y-%m-%d %H:%M:%S}, {sub_buy_order_id=}, {sub_position_id=}  ") 
                    else: # found buy order child 
                        ix = dfx.index[0]
                        ch_buy_real_qty = dfx.loc[ix, 'real_qty']
                        ch_buy_real_price = dfx.loc[ix, 'real_price']
                        ch_leverage_fee = math.ceil((ch_buy_real_price * ch_buy_real_qty) * 0.0004) # leverage fee (0.04%)
    
                        ### update order csv file (accumulated_leverage_fee, close_count, close_update_date)
    
                        # not empty order csv file ?
                        if ord_df.shape[0] > 0:
                            find_mask = ord_df['buy_order_id'] == sub_buy_order_id  # B9999-999999-999999
                            dfx = ord_df[find_mask]
                            # not found order csv file ?
                            if dfx.shape[0] == 0:   
                                self.debug.trace_write(f".... {self.gvar.callers_method_name} {Bcolors.FAIL}not found order csv file{Bcolors.ENDC} : {sub_buy_order_id=}  ") 
                            else: # found order csv file 
                                update_order_csv = False
    
                                ix = dfx.index[0]
                                close_count = ord_df.loc[ix, 'close_count']
                                close_update_date = ord_df.loc[ix, 'close_update_date']
                                accumulated_leverage_fee = ord_df.loc[ix, 'accumulated_leverage_fee']
                                
                                if close_count == 0:
                                    update_order_csv = True
                                    # self.debug.trace_warn(f"DEBUG: update_order_csv={update_order_csv}, close_count={close_count} ")  
                                else: # close_count > 0
                                    now_date_str = f"{dt.datetime.now():%Y%m%d}"            # YYYYMMDD    
                                    close_update_date_str = f"{close_update_date:%Y%m%d}"   # YYYYMMDD                                
                                    if close_update_date_str != now_date_str:
                                        update_order_csv = True
                                    # self.debug.trace_warn(f"DEBUG: update_order_csv={update_order_csv}, close_count={close_count}, now_date_str={now_date_str}, close_update_date_str={close_update_date_str}, leverage_fee={accumulated_leverage_fee:,.0f} ")    
                                    # update_order_csv = True  # DEBUG DEBUG DEBUG
                                # end of if close_count == 0:
    
                                if update_order_csv:
                                    # update order csv file 
                                    ord_df.loc[find_mask, 'accumulated_leverage_fee'] += ch_leverage_fee      
                                    ord_df.loc[find_mask, 'close_count'] += 1
                                    ord_df.loc[find_mask, 'close_update_date'] = dt.datetime.now()    
                                    ord_df.loc[find_mask, 'log_time'] = dt.datetime.now() 
    
                                    ord_df['close_update_date'] = pd.to_datetime(ord_df['close_update_date']) # object => ns time (jp time)  
                                    new_accumulated_leverage_fee = ord_df.loc[ix, 'accumulated_leverage_fee']
                                    new_close_count = ord_df.loc[ix, 'close_count']                               
                                    new_close_update_date = ord_df.loc[ix, 'close_update_date']
                                    self.debug.trace_write(f".... {self.gvar.callers_method_name} update_order_csv_buy(): {sub_buy_order_id=}, {new_accumulated_leverage_fee=:,.0f}, {new_close_count=}, {new_close_update_date=:%Y-%m-%d %H:%M:%S} ") 
                        
                        # end of if ord_df.shape[0] > 0:
                    # end of if dfx.shape[0] == 0:                 
                     
                    # continue to next sell order sub...
                # end of for j in range(len(sell_subx_df)):                                 
    
                # continue to next sell order... ★              
            # end of for i in range(len(sell_df)):  
    
            #################################################################################
            ### 【4】SAVE ORDER CSV TO CSV FILE 
            #################################################################################        
    
            ### CLOSE (save to the order csv file) : order(BTC_JPY).csv
            if ord_df.shape[0] > 0:
                ### CLOSE (order csv file)
                csv_file = self.folder_trading_type + f'order({symbol}).csv'  
                # desktop-pc/bot/buy_sell/order(BTC_JPY).csv   
                ord_df.to_csv(csv_file, header=False, index=False) # overWrite the order file  
    
            return