前回の記事「仮想通貨におけるウォッシュトレード戦略」では、同じ価格で売買することで取引量を積み上げる戦略フレームワークを詳しく説明しました。この戦略の主な目的は、価格裁定取引による利益ではなく、取引所のリベートやティア特典の獲得です。ウォッシュトレード戦略は取引フレームワークを学ぶ上で有益ですが、実際の収益性は限られており、コンプライアンスリスクに直面する可能性があります。
この記事では、前回の記事に基づいて、さらに実践的なマーケット メイク戦略を紹介します。ハンディキャップ戦略ウォッシュ・トレーディング戦略とは異なり、マーケット・メーカー戦略は、買値と売値のスプレッドを設定することで利益を生み出す真の裁定取引戦略であり、従来のマーケット・メーカーの利益モデルに近いものです。
この記事で紹介する市場戦略コードは、学習フレームワークとしてのみ提供されており、実際の取引経験を反映したものではありません。ここで紹介する戦略は、技術的な学習および研究のみを目的としており、実際の市場環境において完全に検証されていません。読者の皆様は、この情報を使用する前に、徹底的なバックテストとリスク評価を実施することをお勧めします。また、実際の取引で直接使用することは避けてください。
成行注文板戦略は、成行注文板(つまり市場の厚み)を裁定取引に利用するマーケットメイク戦略です。この戦略では、マーケットメーカーは市場の変動に基づいて、ビッド価格とアスク価格の差額内で注文価格を動的に調整し、注文を発注・キャンセルすることで、市場流動性と短期的な価格変動を利用して利益を上げます。
| 特徴 | ボリュームブースト戦略 | ハンディキャップ戦略 |
|---|---|---|
| 売買価格 | 同じ価格 | 異なる価格(価格差あり) |
| 利益モデル | 交換リベート/インセンティブ | ビッド・アスク・スプレッド |
| リスクエクスポージャー | 低(同価格取引) | 高(価格変動リスク) |
| 実用性 | 限定 | より高い |
ストライク戦略と一致して、この記事のコードは、主に 2 つのカテゴリに分けられる市場見積もり戦略に基づくマーケット メイク戦略を実装します。
MidClass: 取引所中間層は、取引所インターフェースと対話して市場データ、アカウント情報、注文状況などを取得する役割を担います。MarketMaker: マーケット メイキング戦略クラス。マーケット戦略の実行、保留注文価格の動的な生成、保留注文の生成、注文ステータスの確認、戦略ステータスの更新などを担当します。存在する MarketMakerクラス初期化メソッドでは、まず取引所の精度情報を取得し、取引量の精度、価格の精度などの戦略パラメータを初期化します。
self.precision_info = self.exchange_mid.get_precision() # 获取精度信息
self.price_precision = self.precision_info['price_precision'] # 价格精度
self.amount_precision = self.precision_info['amount_precision'] # 交易量精度
マーケット戦略の核となるのは、買値と売値、そして数量を含む保留注文の辞書を生成することです。このコードは、仲値と価格オフセットを計算することでこの辞書を生成します。
未決注文の価格変動は、価格オフセットを計算することによって実現されます。価格オフセットは価格帯(price_range)と最小価格間隔(min_price_step)を計算します。
price_offset = price_range - self.pending_order_count * min_price_step # 计算价格偏移量
do_trade = price_offset > 0
price_range: 価格帯。保留注文価格と仲値間の偏差範囲を示します。min_price_step: 最小価格間隔。各保留注文価格の最小調整ステップを示します。price_offset: 価格オフセット。現在の注文価格と仲値との差を示します。価格オフセットが 0 より大きい場合、注文を続行できることを意味します。それ以外の場合、保留中の注文の数はリセットされます。
価格オフセットに基づいて、買値と売値が計算され、保留注文の辞書が生成されます。
if do_trade:
buy_price = mid_price - price_offset # 计算买价
buy_price = round(buy_price, self.price_precision) # 四舍五入买价
sell_price = mid_price + price_offset # 计算卖价
sell_price = round(sell_price, self.price_precision) # 四舍五入卖价
trade_dict = {
'do_trade': do_trade,
'buy_price': buy_price,
'sell_price': sell_price,
'amount': trade_amount
}
Log('返回盘口挂单字典:', trade_dict)
return trade_dict
else:
self.pending_order_count = 0 # 重置挂单次数
buy_price: 入札価格。仲値から価格オフセットを差し引いて計算されます。sell_price: ミッド価格に価格オフセットを加えた値として計算される売り価格。trade_dict: 購入価格と販売価格および数量を含む保留注文の辞書。生成された注文辞書に従って、注文トランザクションを実行します。create_order買い注文と売り注文を同時に出す方法です。
def make_trade_by_dict(self, trade_dict):
if trade_dict['do_trade']:
buy_id = self.exchange_mid.create_order('buy', trade_dict['buy_price'], trade_dict['amount']) # 挂买单
sell_id = self.exchange_mid.create_order('sell', trade_dict['sell_price'], trade_dict['amount']) # 挂卖单
self.traded_pairs['pan_kou'].append({
'buy_id': buy_id, 'sell_id': sell_id, 'init_time': time.time(), 'amount': trade_dict['amount']
})
注文状況を定期的に確認し、未約定の注文を処理してください。注文状況の処理ロジックはクロス取引戦略に似ていますが、価格差により、部分的な約定で実際の利益または損失が発生する可能性があります。
1. 保留注文の価格変動が十分に柔軟ではない
この戦略における価格調整メカニズムは比較的単純ですが、次のような制限があります。
price_offset = price_range - self.pending_order_count * min_price_step # 计算价格偏移量
price_range そして min_price_stepこれは固定パラメータであり、市場の状況に応じてリアルタイムで調整することはできません。改善の方向性:
2. 在庫リスク
市場戦略は深刻な在庫リスク問題に直面しています。
リスクパフォーマンス:
コード内のリスクポイント:
if buy_order_status['Status'] == ORDER_STATE_PENDING:
self.sell_amount += traded_pair['amount'] # 只有卖单成交,积累空头持仓
elif sell_order_status['Status'] == ORDER_STATE_PENDING:
self.buy_amount += traded_pair['amount'] # 只有买单成交,积累多头持仓
在庫リスクの影響:
リスク管理措置:
マーケットメイク戦略は、市場の厚みに基づいたマーケットメイク戦略です。売買注文の価格と数量を動的に調整することで、市場流動性を維持します。ウォッシュトレーディング戦略と比較して、マーケットメイク戦略には以下の特徴があります。
✅ メリット
❌ チャレンジ
🔮 今後の最適化の方向性
現在のハンディキャップ戦略の欠点を考慮すると、将来的には以下の方向で最適化および改善することができます。
ダイナミックマーケットメイク戦略:
在庫管理戦略:
マルチレベルマーケットメイク戦略:
スマートリスク管理:
import time, json
class MidClass:
def __init__(self, exchange_instance):
'''
初始化交易所中间层
Args:
exchange_instance: FMZ的交易所结构
'''
self.init_timestamp = time.time() # 记录初始化时间
self.exchange = exchange_instance # 保存交易所对象
self.exchange_name = self.exchange.GetName() # 获取交易所名称
self.trading_pair = self.exchange.GetCurrency() # 获取交易对名称(如 BTC_USDT)
def get_precision(self):
'''
获取交易对的精度信息
Returns:
返回包含精度信息的字典,失败时返回 None
'''
symbol_code = self.exchange.GetCurrency()
ticker = self.exchange.GetTicker(symbol_code) # 回测系统需要
exchange_info = self.exchange.GetMarkets()
data = exchange_info.get(symbol_code)
if not data:
Log("获取市场信息失败", GetLastError())
return None
# 获取该交易对的精度信息
self.precision_info = {
'tick_size': data['TickSize'], # 价格精度
'amount_size': data['AmountSize'], # 数量精度
'price_precision': data['PricePrecision'], # 价格小数位精度
'amount_precision': data['AmountPrecision'], # 数量小数位精度
'min_qty': data['MinQty'], # 最小下单数量
'max_qty': data['MaxQty'] # 最大下单数量
}
return self.precision_info
def get_account(self):
'''
获取账户信息
Returns:
获取信息成功返回 True,获取信息失败返回 False
'''
self.balance = '---' # 账户余额
self.amount = '---' # 账户持仓量
self.frozen_balance = '---' # 冻结余额
self.frozen_stocks = '---' # 冻结持仓量
self.init_balance = None
self.init_stocks = None
self.init_equity = None
try:
account_info = self.exchange.GetAccount() # 获取账户信息
self.balance = account_info['Balance'] # 更新账户余额
self.amount = account_info['Stocks'] # 更新持仓量
self.frozen_balance = account_info['FrozenBalance'] # 更新冻结余额
self.frozen_stocks = account_info['FrozenStocks'] # 更新冻结持仓量
self.equity = self.balance + self.frozen_balance + (self.amount + self.frozen_stocks) * self.last_price
if not self.init_balance or not self.init_stocks or not self.init_equity:
if _G("init_balance") and _G("init_balance") > 0 and _G("init_stocks") and _G("init_stocks") > 0:
self.init_balance = round(_G("init_balance"), 2)
self.init_stocks = round(_G("init_stocks"), 2)
self.init_equity = round(_G("init_equity"), 2)
else:
self.init_balance = round(self.balance + self.frozen_balance, 2)
self.init_stocks = self.amount + self.frozen_stocks
self.init_equity = round(self.init_balance + (self.init_stocks * self.last_price), 2)
_G("init_balance", self.init_balance)
_G("init_stocks", self.init_stocks)
_G("init_equity", self.init_equity)
Log('获取初始eqity', self.init_equity)
self.profit = self.equity - self.init_equity
self.profitratio = round((self.equity - self.init_equity)/self.init_equity, 4) * 100
return True
except:
return False # 获取账户信息失败
def get_ticker(self):
'''
获取市价信息(如买一价、卖一价、最高价、最低价等)
Returns:
获取信息成功返回 True,获取信息失败返回 False
'''
self.high_price = '---' # 最高价
self.low_price = '---' # 最低价
self.sell_price = '---' # 卖一价
self.buy_price = '---' # 买一价
self.last_price = '---' # 最新成交价
self.volume = '---' # 成交量
try:
ticker_info = self.exchange.GetTicker() # 获取市价信息
self.high_price = ticker_info['High'] # 更新最高价
self.low_price = ticker_info['Low'] # 更新最低价
self.sell_price = ticker_info['Sell'] # 更新卖一价
self.buy_price = ticker_info['Buy'] # 更新买一价
self.last_price = ticker_info['Last'] # 更新最新成交价
self.volume = ticker_info['Volume'] # 更新成交量
return True
except:
return False # 获取市价信息失败
def get_depth(self):
'''
获取深度信息(买卖盘的挂单列表)
Returns:
获取信息成功返回 True,获取信息失败返回 False
'''
self.ask_orders = '---' # 卖盘挂单列表
self.bid_orders = '---' # 买盘挂单列表
try:
depth_info = self.exchange.GetDepth() # 获取深度信息
self.ask_orders = depth_info['Asks'] # 更新卖盘挂单列表
self.bid_orders = depth_info['Bids'] # 更新买盘挂单列表
return True
except:
return False # 获取深度信息失败
def get_ohlc_data(self, period=PERIOD_M5):
'''
获取K线信息
Args:
period: K线周期,PERIOD_M1 指1分钟, PERIOD_M5 指5分钟, PERIOD_M15 指15分钟,
PERIOD_M30 指30分钟, PERIOD_H1 指1小时, PERIOD_D1 指一天。
'''
self.ohlc_data = self.exchange.GetRecords(period) # 获取K线数据
def create_order(self, order_type, price, amount):
'''
提交一个挂单信息
Args:
order_type:挂单类型,'buy'指挂买单,'sell'指挂卖单
price:挂单价格
amount:挂单数量
Returns:
挂单Id号,可用以取消挂单
'''
if order_type == 'buy':
try:
order_id = self.exchange.Buy(price, amount) # 提交买单
except:
return False # 买单提交失败
elif order_type == 'sell':
try:
order_id = self.exchange.Sell(price, amount) # 提交卖单
except:
return False # 卖单提交失败
return order_id # 返回订单ID
def get_orders(self):
'''
获取未完成的订单列表
Returns:
未完成的订单列表
'''
self.open_orders = self.exchange.GetOrders() # 获取未完成订单
return self.open_orders
def cancel_order(self, order_id):
'''
取消一个挂单信息
Args:
order_id:希望取消的挂单ID号
Returns:
取消挂单成功返回 True,取消挂单失败返回 False
'''
return self.exchange.CancelOrder(order_id) # 取消订单
def refresh_data(self):
'''
刷新信息(账户、市价、深度、K线)
Returns:
刷新信息成功返回 'refresh_data_finish!' 否则返回相应刷新失败的信息提示
'''
if not self.get_ticker(): # 刷新市价信息
return 'false_get_ticker'
if not self.get_account(): # 刷新账户信息
return 'false_get_account'
if not self.get_depth(): # 刷新深度信息
return 'false_get_depth'
try:
self.get_ohlc_data() # 刷新K线信息
except:
return 'false_get_K_line_info'
return 'refresh_data_finish!' # 刷新成功
class MarketMaker:
def __init__(self, mid_class):
'''
初始化做市策略
Args:
mid_class: 交易所中间层对象
'''
self.exchange_mid = mid_class # 交易所中间层对象
self.precision_info = self.exchange_mid.get_precision() # 获取精度信息
self.done_amount = {'pan_kou': 0} # 已完成交易量
# 修正:正确分配精度信息
self.price_precision = self.precision_info['price_precision'] # 价格精度
self.amount_precision = self.precision_info['amount_precision'] # 交易量精度
self.traded_pairs = {'pan_kou': []} # 已挂单的交易对
self.pending_orders = [] # 未完成的订单状态
self.pending_order_count = 0 # 挂单次数
self.buy_amount = 0
self.sell_amount = 0
self.fee = 0
self.fee_rate = 0.08 / 100
self.chart = {
"__isStock": True,
"tooltip": {"xDateFormat": "%Y-%m-%d %H:%M:%S, %A"},
"title": {"text": "挂单数量"},
"xAxis": {"type": "datetime"},
"yAxis": {
"title": {"text": "挂单数量"},
"opposite": False
},
"series": [
{"name": "挂单买量", "id": "挂单买量", "data": []},
{"name": "挂单卖量", "id": "挂单卖量", "dashStyle": "shortdash", "data": []}
]
}
def refresh_data(self):
'''
刷新数据(账户、市价、深度、K线)
'''
self.exchange_mid.refresh_data() # 刷新交易所数据
self.position_amount = 0 if isinstance(self.exchange_mid.amount, str) else self.exchange_mid.amount # 持仓量
self.available_balance = 0 if isinstance(self.exchange_mid.balance, str) else self.exchange_mid.balance # 账户余额
self.can_buy_amount = self.available_balance / float(self.exchange_mid.buy_price) # 可买的数量
self.mid_price = (self.exchange_mid.sell_price + self.exchange_mid.buy_price) / 2 # 中间价
def make_trade_by_dict(self, trade_dict):
'''
根据交易字典执行交易
Args:
trade_dict: 交易字典
'''
Log('4按照字典开始交易')
self.refresh_data() # 刷新数据
if trade_dict['do_trade']:
Log('当前账户资金: 币数余额: ', self.position_amount, '资金余额: ', self.can_buy_amount)
Log('检查开仓: 币数限制: ', self.position_amount > trade_dict['amount'], '资金限制: ', self.can_buy_amount > trade_dict['amount'])
if self.position_amount > trade_dict['amount'] and self.can_buy_amount > trade_dict['amount']:
buy_id = self.exchange_mid.create_order('buy', trade_dict['buy_price'], trade_dict['amount']) # 挂买单
sell_id = self.exchange_mid.create_order('sell', trade_dict['sell_price'], trade_dict['amount']) # 挂卖单
self.traded_pairs['pan_kou'].append({
'buy_id': buy_id, 'sell_id': sell_id, 'init_time': time.time(), 'amount': trade_dict['amount']
})
self.last_time = time.time() # 更新上次交易时间
def handle_pending_orders(self):
'''
处理未完成的订单
'''
pending_orders = self.exchange_mid.get_orders() # 获取未完成订单
if len(pending_orders) > 0:
for order in pending_orders:
self.exchange_mid.cancel_order(order['Id']) # 取消未完成订单
def make_pankou_dict(self, price_range, min_price_step, trade_amount):
'''
生成盘口挂单字典
Args:
price_range: 价格范围
min_price_step: 最小价格间隔
trade_amount: 每次交易量
Returns:
盘口挂单字典列表
'''
Log('3制作盘口挂单字典', '移动盘口次数', self.pending_order_count)
mid_price = self.mid_price # 中间价
price_offset = price_range - self.pending_order_count * min_price_step # 计算价格偏移量
do_trade = price_offset > 0
if do_trade:
buy_price = mid_price - price_offset # 计算买价
buy_price = round(buy_price, self.price_precision) # 四舍五入买价
sell_price = mid_price + price_offset # 计算卖价
sell_price = round(sell_price, self.price_precision) # 四舍五入卖价
trade_dict = {
'do_trade': do_trade,
'buy_price': buy_price,
'sell_price': sell_price,
'amount': trade_amount
}
Log('返回盘口挂单字典:', trade_dict)
return trade_dict
else:
Log('重置移动盘口次数:', self.pending_order_count)
self.pending_order_count = 0 # 重置移动盘口次数
# 修正:当不能交易时返回None或空字典
return None
def check_order_status(self, current_time):
'''
检查订单状态
Args:
current_time: 当前时间戳
'''
Log('1开始订单信息检查')
Log(self.traded_pairs['pan_kou'])
self.buy_pending = 0
self.sell_pending = 0
for traded_pair in self.traded_pairs['pan_kou'].copy():
Log('检查订单:', traded_pair['buy_id'], traded_pair['sell_id'])
try:
buy_order_status = self.exchange_mid.exchange.GetOrder(traded_pair['buy_id']) # 获取买单状态
sell_order_status = self.exchange_mid.exchange.GetOrder(traded_pair['sell_id']) # 获取卖单状态
except:
Log(traded_pair, '取消')
self.exchange_mid.cancel_order(traded_pair['buy_id']) # 取消买单
self.exchange_mid.cancel_order(traded_pair['sell_id']) # 取消卖单
self.traded_pairs['pan_kou'].remove(traded_pair) # 移除订单
return
Log('检查订单:', traded_pair['buy_id'], buy_order_status, traded_pair['sell_id'], sell_order_status, [sell_order_status['Status'], buy_order_status['Status']])
if [sell_order_status['Status'], buy_order_status['Status']] == [0, 0]:
self.buy_pending += 1
self.sell_pending += 1
if current_time % 5 == 0:
Log('检查挂单,取消挂单(两未完)', buy_order_status['Status'], sell_order_status['Status'], current_time % 5)
self.exchange_mid.cancel_order(traded_pair['buy_id']) # 取消买单
self.exchange_mid.cancel_order(traded_pair['sell_id']) # 取消卖单
self.pending_order_count += 1 # 移动盘口次数次数加1
self.traded_pairs['pan_kou'].remove(traded_pair) # 移除订单
elif {sell_order_status['Status'], buy_order_status['Status']} == {1, 0}:
if buy_order_status['Status'] == ORDER_STATE_PENDING:
self.buy_pending += 1
if sell_order_status['Status'] == ORDER_STATE_PENDING:
self.sell_pending += 1
if current_time % 5 == 0:
Log('检查挂单,取消挂单(一未完)', buy_order_status['Status'], sell_order_status['Status'])
self.done_amount['pan_kou'] += traded_pair['amount'] # 更新交易量
if buy_order_status['Status'] == ORDER_STATE_PENDING:
self.sell_amount += traded_pair['amount']
self.fee += sell_order_status['Amount'] * self.fee_rate * sell_order_status['Price']
Log('取消该买订单,增加未完成买列表', traded_pair['buy_id'])
self.exchange_mid.cancel_order(traded_pair['buy_id']) # 取消买单
self.pending_orders.append(['buy', buy_order_status['Status']]) # 记录未完成订单
Log('清除前:', self.traded_pairs['pan_kou'])
Log('清除id:', traded_pair)
self.traded_pairs['pan_kou'].remove(traded_pair) # 移除订单
Log('清除后:', self.traded_pairs['pan_kou'])
elif sell_order_status['Status'] == ORDER_STATE_PENDING:
self.buy_amount += traded_pair['amount']
self.fee += buy_order_status['Amount'] * self.fee_rate * buy_order_status['Price']
Log('取消该卖订单,增加未完成卖列表', traded_pair['sell_id'])
self.exchange_mid.cancel_order(traded_pair['sell_id']) # 取消卖单
self.pending_orders.append(['sell', sell_order_status['Status']]) # 记录未完成订单
Log('清除前:', self.traded_pairs['pan_kou'])
Log('清除id:', traded_pair)
self.traded_pairs['pan_kou'].remove(traded_pair) # 移除订单
Log('清除后:', self.traded_pairs['pan_kou'])
elif [sell_order_status['Status'], buy_order_status['Status']] == [1, 1]:
Log('两订单都已完成')
Log('完成状态:', buy_order_status['Status'], sell_order_status['Status'], traded_pair['amount'])
self.done_amount['pan_kou'] += 2 * traded_pair['amount'] # 更新交易量
self.buy_amount += traded_pair['amount']
self.sell_amount += traded_pair['amount']
self.fee += buy_order_status['Amount'] * self.fee_rate * buy_order_status['Price']
self.fee += sell_order_status['Amount'] * self.fee_rate * sell_order_status['Price']
self.traded_pairs['pan_kou'].remove(traded_pair) # 移除订单
else:
Log('两订单处于未知状态:', buy_order_status, sell_order_status)
Log('未知订单状态:', buy_order_status['Status'], sell_order_status['Status'])
Log('未知订单信息:', traded_pair)
def update_status(self):
self.exchange_mid.refresh_data()
table1 = {
"type": "table",
"title": "账户信息",
"cols": [
"初始资金", "现存资金", "盘口买入数量", "盘口卖出数量", "费率", "总收益", "收益率"
],
"rows": [
[
self.exchange_mid.init_equity,
self.exchange_mid.equity,
round(self.buy_amount, 4),
round(self.sell_amount, 4),
round(self.fee, 2),
self.exchange_mid.profit,
str(self.exchange_mid.profitratio) + "%"
],
],
}
LogStatus(
f"初始化时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.exchange_mid.init_timestamp))}\n",
f"`{json.dumps(table1)}`\n",
f"最后执行时间: {_D()}\n"
)
LogProfit(round(self.exchange_mid.profit, 3), '&')
def plot_pending(self):
Log('盘口挂单数量:', self.buy_pending, self.sell_pending)
self.obj_chart = Chart(self.chart)
now_time = int(time.time() * 1000)
# 更新挂单买量数据
self.obj_chart.add(0, [now_time, self.buy_pending])
# 更新挂单卖量数据
self.obj_chart.add(1, [now_time, self.sell_pending])
def main():
'''
主函数,运行做市策略
'''
exchange.IO('simulate', True) #OKX现货模拟账户
exchange.IO("trade_super_margin")
current_time = 0
target_amount = 1 # 目标交易量
price_range = 5 # 价格范围
min_price_step = 1 # 最小价格间隔
trade_amount = 0.01 # 每次交易量
exchange_mid = MidClass(exchange) # 初始化交易所中间层
Log(exchange_mid.refresh_data()) # 刷新数据
market_maker = MarketMaker(exchange_mid) # 初始化做市策略
# 修正:初始化trade_dict
trade_dict = None
while market_maker.done_amount['pan_kou'] < target_amount: # 循环直到完成目标交易量
Log(market_maker.traded_pairs['pan_kou'])
market_maker.check_order_status(current_time) # 检查订单状态
Sleep(1000) # 等待1秒
market_maker.refresh_data() # 刷新数据
if len(market_maker.traded_pairs['pan_kou']) < 1: # 价格移动,盘口挂单撤销,等待至所有挂单完毕,制定新的挂单字典
Log('2盘口交易对数量小于1')
trade_dict = market_maker.make_pankou_dict(price_range, min_price_step, trade_amount) # 生成盘口挂单字典
Log('新交易字典', trade_dict)
# 修正:确保trade_dict存在且不为None
if trade_dict and trade_dict.get('do_trade', False):
market_maker.make_trade_by_dict(trade_dict) # 执行交易
Log('盘口做市数量:', market_maker.done_amount['pan_kou']) # 记录交易量
market_maker.plot_pending()
market_maker.update_status()
current_time += 1
Log(market_maker.position_amount, market_maker.can_buy_amount) # 记录持仓量和可买数量
Log('现存订单:', exchange.GetOrders()) # 记录现存订单
def onexit():
Log("执行扫尾函数")
_G("init_balance", None)
_G("init_stocks", None)
_G("init_equity", None)