Python {Article134}

ようこそ「Python」へ...

稼げる仮想通貨を見つけるには〔6〕Binanceの424のコインから累積リターンが337749%のコインを探す

ここでは8回に分けて稼げる仮想通貨を見つける方法について解説します。 第6回目では、バイナンス(Binance)の取引所から仮想通貨のデータをダウンロードして累積リターンのベスト10/ワースト10を表示する方法を解説します。

仮想通貨のシンボルは「2つのシンボル」から構成されています。 GMOコインの場合は「BTC_JPY」のように「_」で区切られています。 「BTC」をベース通貨、「JPY」をクォート通貨と呼びます。 ベース通貨は購入または売却される通貨であり、クォート通貨はベース通貨と取引される通貨です。 Binanceの場合は「BTCUSD」のように区切りがありません。 Binanceから全ての仮想通貨のシンボルを取得するには、ユーザー関数「get_crypto_symbols()」を使用します。 この関数の引数には、クォート通貨のシンボルを指定します。 たとえば、クォート通貨が米ドルのときは、 「get_crypto_symbols('USD')」のように指定します。 引数を省略すると「'USDT'」になります。
###################################################### 
def get_crypto_symbols(quote_currency='USDT') -> list:
    # Define Binance API endpoint
    endpoint = 'https://api.binance.com/api/v3/exchangeInfo'

    # Make a GET request to the endpoint and parse the response as JSON
    response = requests.get(endpoint).json()

    # Extract all symbols that end with the specified quote currency
    symbols = [info['symbol'] for info in response['symbols'] if info['symbol'].endswith(quote_currency)]

    return symbols  
Binanceから全ての仮想通貨のシンボルを取得したらユーザー関数「get_crypto()」を使用して仮想通貨のデータをダウンロードします。 この関数の引数「symbol」には、仮想通貨のシンボルを指定します。デフォルトは「'BTCUSDT'」です。 引数「interval」にはデータのインターバル(1m, 3m, 5m, 1d,...)を指定します。 「m:minute」は「分」、「d:day」は「日」を意味します。デフォルトは「'1d'」です。

引数「limit」には取得するデータの件数を指定します。最大値は「1000」件です。デフォルトは「500」件です。 引数「start」には「開始日」指定します。デフォルトは当日のシステム日付になります。

たとえば、「get_crypto()」のように引数を全て省略すると、 当日(システム日付)を基点(※)に500件のビットコイン(BTCUSDT)の日次(1d)データがダウンロードされてPandasのDataFrameに格納されて返されます。 DataFrameは「open, high, low, close, volume」のカラムから構成されます。 「date」はインデックスに設定されています。

※当日のシステム日付を基点に逆順に500件のデータがダウンロードされることになります。 たとえば、当日が「2023/2/24」なら「2023/2/24, 2023/2/23, 2023/2/22,...」の日次データがダウンロードされます。
####################################################################### 
def get_crypto(symbol='BTCUSDT', interval='1d', limit=500, start=None):
    '''
    interval='1d' 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M    
    limit=500 (default 500, max 1000)        
    '''      
    url = 'https://api.binance.com/api/v3/klines'
          
    params = {
        'symbol': symbol,
        'interval': interval,    
        'limit': str(min(limit, 1000))  # enforce max limit of 1000
    }  

    if start is not None:    
        # add a new element(startTime) to the params dictionary
        params['startTime'] = int(dt.datetime.timestamp(pd.to_datetime(start))*1000) # convert to milliseconds (ms)
    
    try:
        # print(params) 
        # {'symbol': 'BTCUSDT', 'interval': '1d', 'limit': '1000'}
        # {'symbol': 'BTCUSDT', 'interval': '1m', 'limit': '1000', 'startTime': 1672498800000}
        kline  = requests.get(url, params=params)
        kline.raise_for_status()  # raise an error for 4xx and 5xx status codes
        kline_json = kline.json()

        df = pd.DataFrame(kline_json, columns=['date', 'open', 'high', 'low', 'close', 'volume', 'close_time', 
                                               'quote_asset_volume', 'number_of_trades', 'taker_buy_base_asset_volume', 
                                               'taker_buy_quote_asset_volume', '_'])
        df = df.iloc[:, :6]  # only keep the columns for date, OHLC, and volume
        df['date'] = pd.to_datetime(df['date'], unit='ms')
        df = df.set_index('date')
        df = df.astype(float)
        df['symbol'] = symbol
        print(f"get_crypto({symbol=}, {interval}) => {df.shape[0]=}")        
        return df
    except requests.exceptions.HTTPError as e:
        print(f"get_crypto({symbol=}, {interval}) HTTP error: {e}")
    except Exception as e:
        print(f"get_crypto({symbol=}, {interval}) exception error: {e}")
    
    return pd.DataFrame()   


一般に仮想通貨に投資するときは、次のようなメトリック(投資のパフォーマンスや効果を測定するための指標や数値)を使用して判断します。
  • 日次リターン (Daily Return)
    仮想通貨の投資において1日あたりの収益率を示します。
  • ログリターン (Log Return)
    仮想通貨の投資においての対数収益率を示します。
  • 累積ログリターン (Cumulative Log Return)
    仮想通貨の投資においての累積的な対数収益率を示します。
  • トレーディングボリューム (Trading Volume)
    仮想通貨の取引量を示します。
  • マーケットキャップ (Market Cap)
    仮想通貨の時価総額を示します。
  • 価格波動性 (Price Volatility)
    仮想通貨の価格の変動率を示します。
さらに、実際にトレードするときは、次のようなテクニカルインジケーターを複数組み合わせて売買のタイミングを判断します。
  • Simple Moving Average (SMA)
    SMAが上昇傾向であれば買い。
  • Exponential Moving Average (EMA)
    EMAが上昇傾向であれば買い。
  • Bollinger Bands (BB)
    Bollinger Bandsで帯域が狭まっている場合は価格変動が大きいと見られ、買い/売りタイミングに注意。
  • Relative Strength Index (RSI)
    RSIが70以上であれば売り、30以下であれば買い。
  • Stochastic Oscillator (STO)
    STOが80以上であれば売り、20以下であれば買い。
説明文の左側に図の画像が表示されていますが縮小されています。 画像を拡大するにはマウスを画像上に移動してクリックします。 画像が拡大表示されます。拡大された画像を閉じるには右上の[X]をクリックします。 画像の任意の場所をクリックして閉じることもできます。
click image to zoom!
図A
click image to zoom!
図B
click image to zoom!
図C
click image to zoom!
図D
click image to zoom!
図E

Binanceからダウンロードした仮想通貨の累積リターンのベスト10/ワースト10を表示する

  1. まずは、Visual Studio Codeを起動してプログラムファイルを作成する

    Visual Studio Code (VS Code)を起動したら新規ファイル(*.py)を作成して行1-332をコピペします。 ここでは、Jupter NotebookのようにPythonのプログラムをセル単位で実行します。 VS Codeの場合は「#%%」から「#%%」の間がセルになります。 セルを選択したら[Ctrl + Enter」でセルのコードを実行します。 IPythonが起動されて「インタラクティブ」ウィンドウが表示されます。 「インタラクティブ」ウィンドウからはPythonのコードを入力して実行させることができます。 たとえば、「df.info()」を入力して[Shift + Enter」で実行します。

    * Article.py:
    # Daily returns vs Log returns article v71.py (Part 6) : Binance Crypto
    # %%
    
    ### Import pandas, matplotlib, plotly libraries 
    import sys
    import os
    import math
    import numpy as np
    
    import datetime as dt
    import time
    
    import pandas as pd
    
    import matplotlib.pyplot as plt 
    import matplotlib.dates as mdates
    
    import plotly.offline as offline
    import plotly.express as px
    import plotly.graph_objs as go
    
    import datetime as dt
    from datetime import timedelta
    from time import sleep
    
    import requests
    
    import warnings
    warnings.simplefilter('ignore')
    plt.style.use('fivethirtyeight')
    pd.set_option('display.max_rows', 10)
    
    
    # %%
    
    ###################################################### Get all crypto symbols (USDT)
    def get_crypto_symbols(quote_currency='USDT') -> list:
        # Define Binance API endpoint
        endpoint = 'https://api.binance.com/api/v3/exchangeInfo'
    
        # Make a GET request to the endpoint and parse the response as JSON
        response = requests.get(endpoint).json()
    
        # Extract all symbols that end with the specified quote currency
        symbols = [info['symbol'] for info in response['symbols'] if info['symbol'].endswith(quote_currency)]
    
        return symbols
    
    ####################################################################### Load crypto data from binance
    def get_crypto(symbol='BTCUSDT', interval='1d', limit=500, start=None):
        '''
        interval='1d' 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M    
        limit=500 (default 500, max 1000) 
        start='2019-01-01 00:00:00' default       
        '''      
        url = 'https://api.binance.com/api/v3/klines'
              
        params = {
            'symbol': symbol,
            'interval': interval,    
            'limit': str(min(limit, 1000))  # enforce max limit of 1000
        }  
    
        if start is not None:    
            # add a new element(startTime) to the params dictionary
            params['startTime'] = int(dt.datetime.timestamp(pd.to_datetime(start))*1000) # convert to milliseconds (ms)
        
        try:
            # print(params) 
            # {'symbol': 'BTCUSDT', 'interval': '1d', 'limit': '1000'}
            # {'symbol': 'BTCUSDT', 'interval': '1m', 'limit': '1000', 'startTime': 1672498800000}
            kline  = requests.get(url, params=params)
            kline.raise_for_status()  # raise an error for 4xx and 5xx status codes
            kline_json = kline.json()
    
            df = pd.DataFrame(kline_json, columns=['date', 'open', 'high', 'low', 'close', 'volume', 'close_time', 
                                                   'quote_asset_volume', 'number_of_trades', 'taker_buy_base_asset_volume', 
                                                   'taker_buy_quote_asset_volume', '_'])
            df = df.iloc[:, :6]  # only keep the columns for date, OHLC, and volume
            df['date'] = pd.to_datetime(df['date'], unit='ms')
            df = df.set_index('date')
            df = df.astype(float)
            df['symbol'] = symbol
            print(f"get_crypto({symbol=}, {interval}) => {df.shape[0]=}")        
            return df
        except requests.exceptions.HTTPError as e:
            print(f"get_crypto({symbol=}, {interval}) HTTP error: {e}")
        except Exception as e:
            print(f"get_crypto({symbol=}, {interval}) exception error: {e}")
        
        return pd.DataFrame()
    
    ############################################ load crypto data from csv file
    def get_data(csv_file: str) -> pd.DataFrame:
        print(f"Loading data: {csv_file} ")
        df = pd.read_csv(csv_file)       
        # date,open,high,low,close,adj close,volume,symbol
        intervals = {'1d', '3d', '1w', '1M'}    
        found = any(interval in csv_file for interval in intervals)
        if found: # '1d', '3d', '1w', '1M' 
            df['date'] = pd.to_datetime(df['date'])             # convert to a datetime
        else: # 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h
            # df['date'] = pd.to_datetime(df['date']) 
            df['date'] = pd.to_datetime(df['date'], utc=True)   # convert to a timezone-aware UTC-localized Datetime            
        df.set_index(['date'], inplace=True)
        return df   
    
    ############################################################### Calculate cumulative log return
    def calculate_cum_log_return(df: pd.DataFrame) -> pd.DataFrame:
        # Calculate log return
        df['log_return'] = np.log(df['close'] / df['close'].shift(1))  
     
        # Calculate cumulative log return
        df['cum_log_return'] = np.exp(df['log_return'].cumsum()) - 1
        df['cum_log_return_pct'] = df['cum_log_return'] * 100
    
        # Preview the resulting dataframe 
        print(f"Cumulative Log Return for {df.iloc[-1]['symbol']} = {df.iloc[-1]['cum_log_return_pct']:.2%}")     
        return df
    
    #################################
    # Main
    #################################
    
    ### Get all symbols for quote currency (USDT)
    symbols = get_crypto_symbols()
    # symbols # 424
    # print(symbols)
    
    
    # %%
    
    ### Load the data from binance
    
    interval = '1d' # "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1M"
    limit = 1000
    start='now' # now or today
    # start='2023-01-01 00:00:00'
    
    df = pd.DataFrame()
    symbol_list = []
    cum_log_return_list = []
    
    # symbols = ['BTCJPY']   # DEBUG
    
    for symbol in symbols:
        csv_file = f"data/csv/all_binance_cryptocurrencies({symbol})_{interval}.csv"  # data/csv/all_binance_cryptocurrencies(BTCUSDT)_1d.csv
        isFile = os.path.isfile(csv_file)
        if not isFile:   
            # get_crypto(symbol='BTCUSDT', interval='1d', limit=500, start=any)
            if start in 'today, now':
                df = get_crypto(symbol, interval, limit)
            else:
                df = get_crypto(symbol, interval, limit, start)
            if not df.empty:    
                df.to_csv(csv_file, index=True)
            else:
                symbols.remove(symbol)   
        # end of if not isFile:
    
        isFile = os.path.isfile(csv_file)
        if isFile:
            df = get_data(csv_file)
            print(f"{csv_file=}, {df.shape} ")    
            
            if not df.empty:
                df = calculate_cum_log_return(df)
                df.replace([np.inf, -np.inf], np.nan).dropna(axis=1, inplace=True)
                cum_log_return = df.iloc[-1]['cum_log_return']
                symbol_list.append(symbol)
                cum_log_return_list.append(cum_log_return)
    
    if df.empty:
        print(f"Quit the program due to df is empty: {df.empty=}")
        quit() # this is not working for IPython (Jupyter-Notebook)
     
    # print(symbol_list)
    # print(cum_log_return_list)
    
    
    # %%
    
    ### Create DataFrame from dict
    data = {
        'symbol': symbol_list,
        'cum_log_return': cum_log_return_list
    }
    
    raw_df = pd.DataFrame(data)
    # raw_df
    
    
    # %%
    
    raw_df.isnull().sum() 
    
    
    # %%
    
    ### Replace np.inf or -np.inf (positive or negative infinity) with np.nan(Not A Number)
    df = raw_df.replace([np.inf, -np.inf], np.nan)
    ### Drop rows if np.nan (Not A Number)
    df.dropna(axis=0, inplace=True)
    df.isnull().sum() 
    
    
    # %%
    
    raw2_df = df.copy()
    
    ### Print Top or Bottom 10 Cryptocurrencies by Cumulative Log Return : Reset index and add 1 to each index
    best_df = df.nlargest(10, 'cum_log_return')
    worst_df = df.nsmallest(10, 'cum_log_return')
    best_df.reset_index(drop=True, inplace=True) 
    worst_df.reset_index(drop=True, inplace=True) 
    
    # Add 1 to each index
    best_df.index = best_df.index + 1
    worst_df.index = worst_df.index + 1
    
    print('Top 10 Cryptocurrencies by Cumulative Log Return')
    print('-'*60)
    print(best_df)
    print()
    print('Bottom 10 Cryptocurrencies by Cumulative Log Return')
    print('-'*60)
    print(worst_df)
    
    
    # %%
    
    
    best_df = df.nlargest(10, 'cum_log_return')
    worst_df = df.nsmallest(10, 'cum_log_return')
    best_df.reset_index(drop=True, inplace=True) 
    worst_df.reset_index(drop=True, inplace=True) 
    
    # Add 1 to each index
    best_df.index = best_df.index + 1
    worst_df.index = worst_df.index + 1
    
    # Format cum_log_return_pct as a percentage with two decimal places
    best_df['cum_log_return_pct'] = best_df['cum_log_return'].apply(lambda x: '{:.2%}'.format(x))
    worst_df['cum_log_return_pct'] = worst_df['cum_log_return'].apply(lambda x: '{:.2%}'.format(x))
    
    print('Top 10 Cryptocurrencies by Cumulative Log Return')
    print('-'*60)
    print(best_df)
    print()
    print('Bottom 10 Cryptocurrencies by Cumulative Log Return')
    print('-'*60)
    print(worst_df)
    
    
    # %%
    
    ### Print Top or Bottom 10 Cryptocurrencies by Cumulative Log Return : Add gradation
    best_df = df.nlargest(10, 'cum_log_return')
    worst_df = df.nsmallest(10, 'cum_log_return')
    best_df.reset_index(drop=True, inplace=True) 
    worst_df.reset_index(drop=True, inplace=True) 
    
    # Add 1 to each index
    best_df.index = best_df.index + 1
    worst_df.index = worst_df.index + 1
    
    # Format cum_log_return_pct as a percentage with two decimal places
    best_df['cum_log_return_pct'] = best_df['cum_log_return'] * 100
    worst_df['cum_log_return_pct'] = worst_df['cum_log_return'] * 100
    # best_df['cum_log_return_pct'] = best_df['cum_log_return'].apply(lambda x: x * 100)
    # worst_df['cum_log_return_pct'] = worst_df['cum_log_return'].apply(lambda x: x * 100)
    
    # Print Top or Bottom 10 Cryptocurrencies by Cumulative Log Return : Print with gradation
    from IPython.display import HTML
    
    # Apply the background gradient to the dataframe and render it as an HTML table
    best_html_table = best_df.style.background_gradient(subset=['cum_log_return_pct'], axis=0).render()
    # Display the HTML table as an output
    HTML(best_html_table)
    
    # %%
    
    # Apply the background gradient to the dataframe and render it as an HTML table
    worst_html_table = worst_df.style.background_gradient(subset=['cum_log_return_pct'], axis=0).render()
    # Display the HTML table as an output
    HTML(worst_html_table)
    
    
    
    # %%
    
    ### Print Top or Bottom 10 Cryptocurrencies by Cumulative Log Return : df.iterrows() 
    best_df = df.nlargest(10, 'cum_log_return')
    worst_df = df.nsmallest(10, 'cum_log_return')
    best_df.reset_index(inplace=True) 
    worst_df.reset_index(inplace=True) 
    
    # Add 1 to each index
    best_df.index = best_df.index + 1
    worst_df.index = worst_df.index + 1
    
    print('Top 10 Cryptocurrencies by Cumulative Log Return (%)')
    print('-'*70)
    for i, row in best_df.iterrows():    
        cum_log_return = f"{row['cum_log_return']:.2%}"
        print(f"{i}: {row['symbol'].ljust(15)} \t cumulative log return: {cum_log_return.rjust(15)}")
    
    print()
    print('Bottom 10 Cryptocurrencies by Cumulative Log Return (%)')
    print('-'*70)
    for i, row in worst_df.iterrows():    
        cum_log_return = f"{row['cum_log_return']:.2%}"
        print(f"{i}: {row['symbol'].ljust(15)} \t cumulative log return: {cum_log_return.rjust(15)}")
    
    
    # %%
    
    ### Find the specific coin's rank
    best_df = df.nlargest(df.shape[0], 'cum_log_return')
    worst_df = df.nsmallest(df.shape[0], 'cum_log_return')
    
    best_df.reset_index(inplace=True) 
    worst_df.reset_index(inplace=True) 
    
    coin = 'BTCUSDT' # BTCUSDT, ETHUSDT, LTCUSDT, MATICUSDT
    find_coin = best_df['symbol'] == coin
    found_df = best_df[find_coin]
    if found_df.shape[0]:
        print(f"The {coin} coin is ranked {found_df.index.values[0]+1}th.")
    
    
    # %%    
    click image to zoom!
    図1
    図1にはVS Codeの画面が表示されています。 次のステップでは「セル」を選択して「セル」単位でPythonのコードを実行します。
  2. Pythonのライブラリを取り込む

    VS Codeから行5-31のセルをクリックして[Ctrl + Enter]で実行します。 IPythonが起動して「インタラクティブ」ウィンドウに実行結果が表示されます。 ここでは、Python 3.10.9とIPython 8.9.0を使用しています。

    click image to zoom!
    図2
    図2ではPythonの各種ライブラリを取り込んでいます。 行29ではPythonの警告メッセージを抑止しています。 行30ではMatplotlibのデフォルトのスタイルを設定しています。 行31では、PandasのDataFrameを表示するときデータ件数を最大10件に制限しています。
  3. Binanceから424種類の仮想通貨(コイン)のシンボルをダウンロードする

    行37-126のセルを選択したら[Ctrl + Enter]で実行します。 ここでは、Binanceから仮想通貨のシンボルをダウンロードして変数「symbols」に格納しています。

    click image to zoom!
    図3
    図3には仮想通貨のシンボルの件数と内容が表示されています。 「len(symbols)」では「424」が表示されているので、 424種類の仮想通貨のシンボルが格納されていることになります。
  4. 全ての仮想通貨の「2020/5/24~2023/2/17」の範囲の日次データをダウンロードして累積リターンを計算する

    行135-175のセルを選択したら[Ctrl + Enter]で実行します。 ここでは、Binanceから424種類の仮想通貨のデータをダウンロードしてCSVファイルに保存しています。 ダウンロードした仮想通貨のデータから累積リターンを計算して、変数「cum_log_return_list」に格納します。 変数「symbol_list」にはダウンロードした仮想通貨のシンボルを格納します。

    click image to zoom!
    図4-1
    図4-1にはBinanceから仮想通貨のデータをダウンロードしたときのメッセージが表示されています。 仮想通貨のCSVファイル名、データ件数、累積リターンなどが表示されています。
    click image to zoom!
    図4-2
    図4-2にはビットコイン(BTCUSDT)のCSVファイルの内容が表示されています。 CSVファイルには、「date, open, high, low, close, volume, symbol」が格納されています。 日付(date)から日次単位のデータが格納されているのが分かります。
  5. 新規のDataFrame(symbol, cum_log_return)を作成する

    行184-189のセルを選択したら[Ctrl + Enter]で実行します。 ここでは、変数「symbol_list, cum_log_return_list」から新規のDataFrameを生成しています。

    click image to zoom!
    図5
    図5には、DataFrameの構造と内容が表示されています。 DataFrameは「symbol, cum_log_return」のカラムから構成されています。
  6. DataFrameに不正値がある行を削除する

    行195-204のセルを選択したら[Ctrl + Enter]で実行します。 ここでは、DataFrameに不正値(np.inf, np.nan)のある行を削除しています。 np.infは「Infinity」、np.nanは「Not A Number」を意味します。 「Infinity」には「正」と「負」の2種類あります。

    click image to zoom!
    図6
    図6には、DataFrameから不正値を削除する前と後の件数が表示されています。 カラム「cum_log_return」に不正値が「2」件あったのが「0」件になっています。
  7. DataFrameの「nlargest()/nsmallest()」メソッドで仮想通貨のベスト10/ワースト10を表示する

    行209-227のセルを選択して実行します。 ここでは、DataFrameに格納されている仮想通貨を「累積リターン」で並べ替えてベスト10/ワースト10を表示しています。

    click image to zoom!
    図7-1
    図7-1には累積リターンのベスト10/ワースト10が表示されています。 ここでは「print()」でDataFrameの内容を表示しています。 DataFrameは「symbol, cum_log_return」から構成されています。 インデックスには「1」から始まる連番が採番されています。 DataFrameの「nlargest(), nsmallest()」メソッドで並べ替えているので、 上位10件と下位10件の仮想通貨が表示されます。
    click image to zoom!
    図7-2
    図7-2には「累積リターン」がパーセント「%」付きで表示されています。 ここでは、DataFrameの「apply()」メソッドを使用して「累積リターン」をパーセント(%)の形式に変換しています。
    click image to zoom!
    図7-3
    図7-3にはDataFrameのカラム「cum_log_return_pct」がグラデーション付きで表示されています。 DataFrameのカラムをグラデーション付きで表示するには、DataFrameの「style.background_gradient()」メソッドを使用します。 スタイル付きのDataFrameを表示するには、IPython.displayの「HTML()」メソッドを使用します。
    click image to zoom!
    図7-4
    図7-4では、DataFrameの「iterrows()」メソッドを使用して、 DataFrameから行(row)単位でレコード(データ)を取り出して表示しています。 この場合、ベスト10/ワースト10のレイアウトを自由にカスタマイズすることができます。 「print()」で表示するとき、カラムの位置を揃えるのに、 Pythonの「ljust(), rjust()」を使っています。 さらにタブ「\t」も使用しています。
  8. 特定の仮想通貨のランキングを調べる

    行319-329のセルを選択したら[Ctrl + Enter]で実行します。 ここではランキング外の仮想通貨の順位を表示させます。

    click image to zoom!
    図8
    図8にはビットコイン(BTCUSDT)のランキング(順位)を表示させています。 ビットコインの順位(上位)は「61」位になっています。