c_mexc_to_erc20.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. import requests
  2. import decimal
  3. import time
  4. import threading
  5. import json
  6. import logging
  7. import ok_chain_client # 假设这是你的 OKX Chain 客户端库
  8. import pprint
  9. import plotly.graph_objects as go
  10. import argparse
  11. from flask import Flask, render_template, jsonify
  12. from collections import deque
  13. from plotly.utils import PlotlyJSONEncoder
  14. # configs
  15. from config import wallet
  16. from config import okchain_api
  17. from config import arb
  18. # logs
  19. from logger_config import get_logger
  20. logger = get_logger('as')
  21. # lite客户端
  22. from web3_py_client_lite import EthClient
  23. web3_client = EthClient()
  24. # ok web3的配置
  25. ok_chain_client.api_config = okchain_api # 假设ok_chain_client有此配置方式
  26. # --- 配置 arb_executor.py 的 HTTP 地址和端口 ---
  27. ARB_EXECUTOR_URL = arb["ARB_EXECUTOR_URL"]
  28. # --- 配置部分 ---
  29. # EXCHANGE_OUT_AMOUNT 将在循环中动态确定
  30. IN_AMOUNT_TO_QUERY = decimal.Decimal(str(arb["COIN_TOKEN_TRADE_AMOUNT"]))
  31. PROFIT_LIMIT = decimal.Decimal(str(arb["PROFIT_LIMIT"])) # 确保是Decimal
  32. IN_TOKEN_ADDRESS = arb["COIN_TOKEN_ADDRESS"]
  33. IN_TOKEN_DECIMALS = web3_client.get_erc20_decimals(IN_TOKEN_ADDRESS)
  34. OUT_TOKEN_ADDRESS = arb["BASE_TOKEN_ADDRESS"]
  35. SLIPPAGE = arb["SLIPPAGE"]
  36. MEXC_TARGET_PAIR_USDT = arb["CEX_PAIR"]
  37. CHAIN_ID = arb["CHAIN_ID"]
  38. STRATEGY = arb["STRATEGY"]
  39. # 錢包的配置
  40. USER_WALLET = wallet["user_wallet"]
  41. USER_EXCHANGE_WALLET = wallet["user_exchange_wallet"]
  42. proxies = None # {'http': 'http://proxy_url:port', 'https': 'http://proxy_url:port'}
  43. # 運行模式【trade、view】
  44. mode = None
  45. # chain_price = None # 这个全局变量似乎没有被有效使用,价格在循环内获取
  46. # 配置請求的日志等級
  47. app = Flask(__name__)
  48. log = logging.getLogger('werkzeug')
  49. log.setLevel(logging.ERROR)
  50. REFRESH_INTERVAL_SECONDS = 1 # 稍微增加间隔以减少API调用频率
  51. MAX_HISTORY_POINTS_PLOTLY = 21600
  52. historical_data_points = deque(maxlen=MAX_HISTORY_POINTS_PLOTLY)
  53. TARGET_ASSET_SYMBOL = MEXC_TARGET_PAIR_USDT.split('_')[0] # e.g., RATO
  54. BASE_CURRENCY_SYMBOL = MEXC_TARGET_PAIR_USDT.split('_')[1] # e.g., USDT (assumed to be consistent with IN_TOKEN_ADDRESS)
  55. # --- 链上价格获取函数 (链上) ---
  56. # 返回: price_base_per_target (例如 USDT per RATO)
  57. def get_chain_price_vs_target_currency(chain_id, in_token_addr, out_token_addr, amount_in_base_human, in_token_decimals, slippage, user_wallet_addr, user_exchange_wallet_addr):
  58. try:
  59. # amount_in_base_human 已经是 decimal.Decimal 类型的人类可读数量
  60. in_token_amount_atomic = int(amount_in_base_human * (10 ** in_token_decimals)) # 转为原子单位整数
  61. data = ok_chain_client.swap(chain_id, in_token_amount_atomic, in_token_addr, out_token_addr, slippage, user_wallet_addr, user_exchange_wallet_addr, 'fast')
  62. if data.get('code') == '0' and data.get('data'):
  63. d = data['data'][0]
  64. router_result = d['routerResult']
  65. in_dec, out_dec = int(router_result['fromToken']['decimal']), int(router_result['toToken']['decimal'])
  66. atomic_in_base, atomic_out_target = decimal.Decimal(router_result['fromTokenAmount']), decimal.Decimal(router_result['toTokenAmount'])
  67. human_in_target = atomic_in_base / (10 ** in_dec)
  68. human_out_base = atomic_out_target / (10 ** out_dec)
  69. if human_out_base == 0: return {"error": f"OO输出目标代币为0 ({CHAIN_ID})"}, data, 0 # data 也返回
  70. # 1target = x usdt,这个价格
  71. return {"price_base_per_target": human_out_base / human_in_target }, data, human_out_base
  72. else:
  73. pprint.pprint(data)
  74. return {
  75. "error": f"链上 API错误({chain_id}) - Code:{data.get('code', 'N/A')}, Msg:{data.get('msg', data.get('message', 'N/A')) if isinstance(data, dict) else '格式错误'}"}, None, 0
  76. except Exception as e:
  77. logger.error(f"链上 ({chain_id})请求错误详情: ", exc_info=True)
  78. return {"error": f"链上 ({chain_id})请求错误: {e}"}, None, 0
  79. # MEXC 现货 (获取 目标代币/USDT 的 bid 价格)
  80. # human_out_base: 需要买入大概多少价值的target
  81. def get_mexc_spot_price_target_usdt_ask(human_out_base):
  82. url = "https://api.mexc.com/api/v3/depth"
  83. params = {'symbol': MEXC_TARGET_PAIR_USDT.replace('_', ''), 'limit': 1000}
  84. try:
  85. r = requests.get(url, params=params, proxies=proxies, timeout=5) # 减少超时
  86. r.raise_for_status()
  87. data = r.json()
  88. if 'asks' in data and data['asks']: # 确保asks存在且不为空
  89. asks = data['asks']
  90. trade_value_remaining = human_out_base # 还需要买入的价值(USDT)
  91. trade_value = decimal.Decimal('0') # 累计的总价值 (Decimal)
  92. accumulated_volume = decimal.Decimal('0') # 累计吃单量
  93. for orderbook in asks:
  94. price = decimal.Decimal(orderbook[0])
  95. volume = decimal.Decimal(orderbook[1])
  96. if trade_value_remaining <= decimal.Decimal('0'):
  97. break # 已经满足买入量
  98. can_fill = min(volume, trade_value_remaining / price)
  99. trade_value += price * can_fill
  100. accumulated_volume += can_fill
  101. trade_value_remaining -= price * can_fill
  102. # print(trade_value, accumulated_volume, trade_value_remaining)
  103. # print('\n\n')
  104. if accumulated_volume == decimal.Decimal('0'): # 如果一点都没买入去
  105. return {"error": f"MEXC订单簿深度不足以买入{human_out_base} {BASE_CURRENCY_SYMBOL}"}, decimal.Decimal('0')
  106. # 计算平均买入价格
  107. # buy_price 代表 1 TARGET_ASSET = X USDT
  108. buy_price = trade_value / accumulated_volume
  109. buy_price = buy_price.quantize(decimal.Decimal('1e-10'), rounding=decimal.ROUND_DOWN)
  110. # trade_value 代表买入 accumulated_volume 个 TARGET_ASSET 能得到的 TARGET 总量
  111. return {
  112. "price_target_per_base": buy_price
  113. }, trade_value # 返回的是实际能买入 EXCHANGE_OUT_AMOUNT (或更少,如果深度不足) 所得的 USDT 总额
  114. else:
  115. # logger.warning(f"MEXC现货({MEXC_TARGET_PAIR_USDT}) asks 数据不存在或为空: {data}")
  116. return {"error": f"MEXC现货({MEXC_TARGET_PAIR_USDT}) asks 数据不存在或为空"}, decimal.Decimal('0')
  117. except requests.exceptions.RequestException as e:
  118. # logger.error(f"MEXC现货({MEXC_TARGET_PAIR_USDT})请求错误: {e}")
  119. return {"error": f"MEXC现货({MEXC_TARGET_PAIR_USDT})请求错误: {e}"}, decimal.Decimal('0')
  120. except Exception as e:
  121. # logger.error(f"MEXC现货({MEXC_TARGET_PAIR_USDT})处理错误: {e}", exc_info=True)
  122. return {"error": f"MEXC现货({MEXC_TARGET_PAIR_USDT})处理错误: {e}"}, decimal.Decimal('0')
  123. latest_values_for_table = {
  124. f"chain_price": "N/A",
  125. f"cex_price": "N/A", # MEXC price (converted to USDT/TARGET)
  126. f"diff_oo_vs_mexc_percentage": "N/A",
  127. "profit_value_for_table": "N/A", # 新增:用于表格的利润值
  128. "oo_error": None, "mexc_error": None,
  129. "last_updated": "N/A",
  130. "mexc_pair_usdt_for_display": MEXC_TARGET_PAIR_USDT,
  131. "target_asset_symbol_for_display": TARGET_ASSET_SYMBOL,
  132. "base_currency_symbol_for_display": BASE_CURRENCY_SYMBOL
  133. }
  134. data_lock = threading.Lock()
  135. def calculate_percentage_diff(sell_price, buy_price):
  136. # 期望 sell_price > buy_price
  137. if sell_price is not None and buy_price is not None and \
  138. isinstance(sell_price, decimal.Decimal) and \
  139. isinstance(buy_price, decimal.Decimal) and buy_price != 0:
  140. # (卖价 - 买价) / 买价
  141. rst = (sell_price - buy_price) / buy_price
  142. rst = rst.quantize(decimal.Decimal('1e-6'), rounding=decimal.ROUND_DOWN) # 提高精度
  143. return rst
  144. return None
  145. def send_arb_msg(profit_amount, chain_swap_data, human_in_base, human_out_base, chain_price, cex_price):
  146. # chain_swap_data 是从 get_chain_price_vs_target_currency 返回的第二个值
  147. if not (chain_swap_data and chain_swap_data.get('data') and chain_swap_data['data']):
  148. logger.error(f"套利消息发送失败:链上交易数据不完整 {chain_swap_data}")
  149. return
  150. d = chain_swap_data['data'][0]
  151. tx = d['tx'] # 这是预签名的交易结构体,不是tx hash
  152. router_result = d['routerResult']
  153. from_token_info = router_result['fromToken']
  154. to_token_info = router_result['toToken']
  155. in_dec, out_dec = int(from_token_info['decimal']), int(to_token_info['decimal'])
  156. # human_in_target 根据实际传入的 IN_AMOUNT_TO_QUERY (trade_value) 确定
  157. # human_out_base 是链上swap的实际输出
  158. atomic_out_target = decimal.Decimal(router_result['toTokenAmount'])
  159. human_out_base = atomic_out_target / (10 ** out_dec)
  160. arbitrage_data = {
  161. "tx": tx, # 预签名交易
  162. "profit": str(profit_amount.quantize(decimal.Decimal('0.001'))),
  163. "profitLimit": str(PROFIT_LIMIT.quantize(decimal.Decimal('0.001'))),
  164. "symbol": MEXC_TARGET_PAIR_USDT,
  165. "fromToken": IN_TOKEN_ADDRESS,
  166. "fromTokenAmountHuman": str(human_in_base.quantize(decimal.Decimal(f'1e-{in_dec}'))),
  167. "fromTokenDecimal": str(in_dec),
  168. "toToken": OUT_TOKEN_ADDRESS,
  169. "toTokenAmountHuman": str(human_out_base.quantize(decimal.Decimal(f'1e-{out_dec}'))),
  170. "toTokenDecimal": str(out_dec),
  171. "chainPrice": str(chain_price),
  172. "cexPrice": str(cex_price),
  173. "strategy": STRATEGY,
  174. }
  175. logger.info(f"正在提交套利数据到 {ARB_EXECUTOR_URL}, profit {arbitrage_data["profit"]}, profitLimit {arbitrage_data["profitLimit"]}")
  176. try:
  177. response = requests.post(ARB_EXECUTOR_URL, json=arbitrage_data, timeout=10)
  178. logger.info(f"套利执行器响应状态码: {response.status_code}")
  179. try:
  180. response_data = response.json()
  181. logger.info(f"套利执行器响应内容: {response_data}")
  182. except requests.exceptions.JSONDecodeError:
  183. logger.error(f"套利执行器响应无法解析为JSON: {response.text}")
  184. except requests.exceptions.RequestException as e:
  185. logger.error(f"连接套利执行器 {ARB_EXECUTOR_URL} 失败: {e}")
  186. except Exception as e:
  187. logger.error(f"发送套利消息未知错误: {e}", exc_info=True)
  188. def update_data_for_plotly_and_table():
  189. global historical_data_points, latest_values_for_table # IN_AMOUNT_TO_QUERY
  190. logger.info(f"数据更新线程 ({TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL})...")
  191. # local_in_amount_to_query = decimal.Decimal(str(arb["IN_AMOUNT_TO_QUERY"])) # 从配置初始化,后续动态调整
  192. while True:
  193. fetch_time_full = time.strftime("%Y-%m-%d %H:%M:%S")
  194. fetch_time_chart = time.strftime("%H:%M:%S")
  195. # 1. 获取链上价格:卖出 IN_AMOUNT_TO_QUERY 这么多的TARGET去卖成USDT,能卖到多少,以及价格 (USDT/TARGET)
  196. oo_data, chain_swap_full_response, human_out_base = get_chain_price_vs_target_currency(
  197. CHAIN_ID,
  198. IN_TOKEN_ADDRESS, # TARGET
  199. OUT_TOKEN_ADDRESS, # USDT
  200. IN_AMOUNT_TO_QUERY, # 链上卖出代币量
  201. IN_TOKEN_DECIMALS, # 链上卖出代币的精度
  202. SLIPPAGE,
  203. USER_WALLET,
  204. USER_EXCHANGE_WALLET
  205. )
  206. chain_price = oo_data.get("price_base_per_target") # USDT/TARGET
  207. oo_err = oo_data.get("error")
  208. # 2. MEXC: 获取 price_target_per_base (例如 RATO/USDT)
  209. # trade_value_usdt 是指如果以 EXCHANGE_OUT_AMOUNT 的目标代币在MEXC上砸盘卖出,能获得的USDT估值
  210. mexc_data, trade_value_usdt = get_mexc_spot_price_target_usdt_ask(human_out_base)
  211. cex_price = mexc_data.get("price_target_per_base") # TARGET/USDT
  212. mexc_err = mexc_data.get("error")
  213. # 3. 计算百分比差异
  214. # diff = (链上卖价 - MEXC买价) / MEXC买价
  215. diff_oo_vs_mexc_pct = calculate_percentage_diff(
  216. chain_price, # 链上卖价 (USDT/TARGET)
  217. cex_price, # MEXC买价 (USDT/TARGET)
  218. )
  219. # 4. 计算实际利润额 (以USDT计价)
  220. # 简化:利润百分比 * 投入的USDT金额
  221. actual_profit_usdt = None
  222. if diff_oo_vs_mexc_pct is not None and chain_price is not None and chain_price > 0:
  223. # 基于百分比和投入金额
  224. actual_profit_usdt = diff_oo_vs_mexc_pct * human_out_base
  225. # 5. 满足利润条件,发送套利消息, PROFIT_LIMIT + 3的3是提前計算的成本,否則一直提交
  226. global mode
  227. if actual_profit_usdt is not None and actual_profit_usdt > PROFIT_LIMIT + 3 and mode == 'trade':
  228. if chain_swap_full_response: # 确保有完整的链上数据
  229. human_in_base = IN_AMOUNT_TO_QUERY
  230. send_arb_msg(actual_profit_usdt, chain_swap_full_response, human_in_base, human_out_base, chain_price, cex_price)
  231. else:
  232. logger.warning("利润满足但链上数据不完整,无法发送套利消息。")
  233. current_point = {
  234. "time": fetch_time_chart,
  235. "chain_price": float(chain_price) if chain_price else None,
  236. "cex_price": float(cex_price) if cex_price else None,
  237. "diff_oo_vs_mexc": float(diff_oo_vs_mexc_pct) if diff_oo_vs_mexc_pct is not None else None,
  238. "profit_value": float(actual_profit_usdt) if actual_profit_usdt is not None else None, # 新增:用于图表的实际利润额
  239. }
  240. with data_lock:
  241. historical_data_points.append(current_point)
  242. latest_values_for_table["chain_price"] = f"{chain_price:.8f}" if chain_price else "N/A"
  243. latest_values_for_table["cex_price"] = f"{cex_price:.8f}" if cex_price else "N/A"
  244. latest_values_for_table["diff_oo_vs_mexc_percentage"] = f"{diff_oo_vs_mexc_pct:+.4%}" if diff_oo_vs_mexc_pct is not None else "N/A" # 显示为百分比
  245. latest_values_for_table["profit_value_for_table"] = f"{actual_profit_usdt:.2f} {BASE_CURRENCY_SYMBOL}" if actual_profit_usdt is not None else "N/A" # 新增
  246. latest_values_for_table["oo_error"] = oo_err
  247. latest_values_for_table["mexc_error"] = mexc_err
  248. latest_values_for_table["last_updated"] = fetch_time_full
  249. latest_values_for_table["in_amount_for_query_display"] = f"{human_out_base:.2f} {BASE_CURRENCY_SYMBOL}" if human_out_base > 0 else "N/A"
  250. # logger.info(f"{fetch_time_chart} Price Query: Chain Input {human_out_base:.2f} {BASE_CURRENCY_SYMBOL} | OKX Price: {chain_price_display} | MEXC Price: {cex_price_display} | Diff: {diff_display} | Profit: {profit_display}")
  251. if oo_err or mexc_err :
  252. logger.warning(f"{fetch_time_chart} Errors: OO:{oo_err}, MEXC:{mexc_err}")
  253. time.sleep(REFRESH_INTERVAL_SECONDS)
  254. @app.route('/')
  255. def index_plotly():
  256. return render_template('index_plotly_dynamic_ok.html',
  257. target_asset=TARGET_ASSET_SYMBOL,
  258. base_asset=BASE_CURRENCY_SYMBOL,
  259. mexc_pair_usdt=MEXC_TARGET_PAIR_USDT,
  260. refresh_interval_ms=REFRESH_INTERVAL_SECONDS * 1000)
  261. @app.route('/table-data')
  262. def get_table_data():
  263. with data_lock:
  264. # logger.info(f"Table data requested: {latest_values_for_table}")
  265. return jsonify(latest_values_for_table)
  266. @app.route('/plotly-chart-data')
  267. def get_plotly_chart_data():
  268. with data_lock:
  269. points = list(historical_data_points)
  270. # logger.info(f"Chart data requested, {len(points)} points.")
  271. if not points:
  272. fig = go.Figure() # Create an empty figure
  273. fig.update_layout(title_text="暂无数据")
  274. empty_json = json.loads(json.dumps(fig, cls=PlotlyJSONEncoder))
  275. return jsonify({
  276. "price_chart": empty_json,
  277. "diff_chart": empty_json,
  278. "profit_chart": empty_json # 新增:空利润图表
  279. })
  280. times = [p['time'] for p in points]
  281. display_target_asset = latest_values_for_table["target_asset_symbol_for_display"]
  282. display_base_asset = latest_values_for_table["base_currency_symbol_for_display"]
  283. common_xaxis_config = dict(title='时间')
  284. common_legend_config = dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
  285. # if len(times) > 1: # Plotly handles autorange for single point ok
  286. # common_xaxis_config['range'] = [times[0], times[-1]]
  287. # else:
  288. common_xaxis_config['autorange'] = True
  289. # Price Chart
  290. fig_prices = go.Figure()
  291. fig_prices.add_trace(go.Scatter(x=times, y=[p['chain_price'] for p in points], mode='lines',
  292. name=f'链上({display_base_asset}/{display_target_asset})',
  293. line=dict(color='rgb(75, 192, 192)'),
  294. hovertemplate=f'<b>链上价</b><br>价格: %{{y:.8f}} {display_base_asset}<extra></extra>',
  295. connectgaps=True)) # 处理None值不画线
  296. fig_prices.add_trace(go.Scatter(x=times, y=[p['cex_price'] for p in points], mode='lines',
  297. name=f'MEXC价 ({display_base_asset}/{display_target_asset})',
  298. line=dict(color='rgb(255, 99, 132)', dash='dash'),
  299. hovertemplate=f'<b>MEXC价</b><br>价格: %{{y:.8f}} {display_base_asset}<extra></extra>',
  300. connectgaps=True))
  301. fig_prices.update_layout(title_text=f'{display_base_asset}/{display_target_asset} 价格历史',
  302. xaxis=common_xaxis_config.copy(),
  303. yaxis_title=f'价格 (1 {display_target_asset} = X {display_base_asset})',
  304. legend_title_text='平台',
  305. legend=common_legend_config.copy(), hovermode='x unified',
  306. margin=dict(l=70, r=30, t=80, b=50))
  307. # Percentage Difference Chart
  308. fig_diffs = go.Figure()
  309. fig_diffs.add_trace(
  310. go.Scatter(x=times, y=[p['diff_oo_vs_mexc'] for p in points], mode='lines', name=f'价差百分比 (MEXC卖价 vs 链上买价)',
  311. line=dict(color='rgb(255, 159, 64)'),
  312. hovertemplate=f'<b>(MEXC卖价-链上买价)/链上买价</b><br>百分比: %{{y:+.4%}}<extra></extra>', # 显示为百分比
  313. connectgaps=True))
  314. fig_diffs.update_layout(title_text=f'价差百分比历史曲线',
  315. xaxis=common_xaxis_config.copy(),
  316. yaxis_title='价差百分比', legend_title_text='对比', legend=common_legend_config.copy(),
  317. yaxis_zeroline=True, hovermode='x unified', margin=dict(l=70, r=30, t=80, b=50),
  318. yaxis_tickformat=".4%") # y轴也显示为百分比
  319. # --- 新增 Profit Chart ---
  320. fig_profit = go.Figure()
  321. fig_profit.add_trace(
  322. go.Scatter(x=times, y=[p['profit_value'] for p in points], mode='lines', name=f'预估利润 ({display_base_asset})',
  323. line=dict(color='rgb(153, 102, 255)'), # 紫色
  324. hovertemplate=f'<b>预估利润</b><br>金额: %{{y:,.2f}} {display_base_asset}<extra></extra>', # 利润金额,保留2位小数
  325. connectgaps=True))
  326. fig_profit.update_layout(title_text=f'预估利润历史 ({display_base_asset})',
  327. xaxis=common_xaxis_config.copy(),
  328. yaxis_title=f'利润 ({display_base_asset})',
  329. legend_title_text='利润额',
  330. legend=common_legend_config.copy(),
  331. yaxis_zeroline=True, hovermode='x unified',
  332. margin=dict(l=70, r=30, t=80, b=50),
  333. yaxis_tickformat="$,.2f") # y轴格式化为货币
  334. combined_figure_data = {
  335. "price_chart": json.loads(json.dumps(fig_prices, cls=PlotlyJSONEncoder)),
  336. "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder)),
  337. "profit_chart": json.loads(json.dumps(fig_profit, cls=PlotlyJSONEncoder)) # 新增
  338. }
  339. return jsonify(combined_figure_data)
  340. if __name__ == "__main__":
  341. parser = argparse.ArgumentParser(description='套利监控和交易脚本。')
  342. parser.add_argument('--mode',
  343. required=True,
  344. choices=['trade', 'view'], # 限制可选值
  345. help='运行模式: "trade" (执行交易) 或 "view" (仅观察)')
  346. except_strategy = 'mexc_to_erc20'
  347. if STRATEGY != except_strategy:
  348. raise Exception(f"策略不匹配! 期待{except_strategy}, 实际{STRATEGY}")
  349. try:
  350. args = parser.parse_args()
  351. mode = args.mode
  352. logger.info(f"脚本运行模式为: {mode}")
  353. logger.info("应用启动...")
  354. logger.info(f"目标资产: {TARGET_ASSET_SYMBOL}, 计价货币: {BASE_CURRENCY_SYMBOL}, 获取到的Decimal: {IN_TOKEN_DECIMALS}")
  355. # IN_AMOUNT_TO_QUERY 会动态变化,初始值从配置读取,但循环中会基于MEXC的trade_value更新
  356. # logger.info(f"链上查询初始金额: {arb['IN_AMOUNT_TO_QUERY']} {BASE_CURRENCY_SYMBOL} -> {TARGET_ASSET_SYMBOL}")
  357. logger.info(f"链上期望卖出量 (用于计算深度和价值): {IN_AMOUNT_TO_QUERY} {TARGET_ASSET_SYMBOL}")
  358. logger.info(f"利润阈值: {PROFIT_LIMIT} {BASE_CURRENCY_SYMBOL}")
  359. logger.info(f"MEXC现货交易对: {MEXC_TARGET_PAIR_USDT}")
  360. data_thread = threading.Thread(target=update_data_for_plotly_and_table, daemon=True)
  361. data_thread.start()
  362. port = arb.get("PORT", 5001) # 从配置获取端口,如果没有则默认5001
  363. logger.info(f"Flask 服务将在 http://0.0.0.0:{port} 上运行 (刷新间隔: {REFRESH_INTERVAL_SECONDS}s)")
  364. app.run(debug=False, host='0.0.0.0', port=port, use_reloader=False)
  365. except SystemExit: # argparse 在参数错误时会引发 SystemExit
  366. # parser.print_help() # argparse 默认会打印帮助信息
  367. logger.info("脚本因参数错误而退出。请提供 '--mode' 参数 ('trade' 或 'view')。")
  368. except Exception as e:
  369. logger.critical(f"主程序发生严重错误: {e}", exc_info=True)