price_checker.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import requests
  2. import decimal
  3. import time
  4. import threading
  5. import json
  6. from flask import Flask, render_template, jsonify
  7. from collections import deque
  8. import logging
  9. import plotly
  10. import plotly.graph_objects as go
  11. from plotly.utils import PlotlyJSONEncoder
  12. # --- 配置部分 (与你提供的一致) ---
  13. IN_TOKEN_ADDRESS_ETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
  14. OUT_TOKEN_ADDRESS_TARGET_ETH = '0xf816507E690f5Aa4E29d164885EB5fa7a5627860'
  15. AMOUNT_TO_QUERY_OPENOCEAN_IN_ETH = decimal.Decimal('1')
  16. MEXC_TARGET_CONTRACT_PAIR_USDT = 'RATO_USDT' # 重命名以明确是合约
  17. BINANCE_BASE_CURRENCY_PAIR_USDT = 'ETHUSDT'
  18. proxies = None
  19. decimal.getcontext().prec = 36
  20. # --- 价格获取函数 ---
  21. def get_openocean_price_vs_base_currency(chain_name, in_token_addr, out_token_addr, human_amount_in_base_currency):
  22. url = f'https://open-api.openocean.finance/v4/{chain_name}/quote'
  23. gas_price_value = '50000000000' if chain_name == 'eth' else '0.000005'
  24. params = {'inTokenAddress': in_token_addr, 'outTokenAddress': out_token_addr,
  25. 'amount': str(human_amount_in_base_currency), 'gasPrice': gas_price_value}
  26. try:
  27. r = requests.get(url, params=params, proxies=proxies, timeout=10);
  28. r.raise_for_status();
  29. data = r.json()
  30. if data.get('code') == 200 and data.get('data'):
  31. d = data['data'];
  32. req = ['inToken', 'outToken', 'inAmount', 'outAmount']
  33. if not all(k in d for k in req) or d['inToken'].get('decimals') is None or d['outToken'].get(
  34. 'decimals') is None: return {"error": "OO API缺少数据"}
  35. in_dec, out_dec = int(d['inToken']['decimals']), int(d['outToken']['decimals']);
  36. atomic_in_base, atomic_out_target = decimal.Decimal(d['inAmount']), decimal.Decimal(d['outAmount'])
  37. human_in_base = atomic_in_base / (10 ** in_dec);
  38. human_out_target = atomic_out_target / (10 ** out_dec)
  39. if human_out_target == 0: return {"error": f"OO输出目标代币为0 ({chain_name})"}
  40. return {"price_base_per_target": human_in_base / human_out_target}
  41. else:
  42. return {
  43. "error": f"OO API错误({chain_name}) - Code:{data.get('code', 'N/A')}, Msg:{data.get('msg', data.get('message', 'N/A')) if isinstance(data, dict) else '格式错误'}"}
  44. except Exception as e:
  45. return {"error": f"OO({chain_name})请求错误: {e}"}
  46. # MEXC 合约,同时获取 ask1 和 bid1
  47. def get_mexc_contract_ticker_usdt(pair_symbol): # Changed name for clarity
  48. url = "https://contract.mexc.com/api/v1/contract/ticker"
  49. # MEXC合约交易对通常是 BASE_QUOTE,例如 RATO_USDT
  50. params = {'symbol': pair_symbol}
  51. try:
  52. r = requests.get(url, params=params, proxies=proxies, timeout=10);
  53. r.raise_for_status();
  54. response_data = r.json()
  55. if response_data.get('success') and response_data.get('data'):
  56. data = response_data['data']
  57. if data.get('symbol') == params['symbol'] and 'ask1' in data and 'bid1' in data:
  58. return {
  59. "price_target_usdt_ask1": decimal.Decimal(data['ask1']),
  60. "price_target_usdt_bid1": decimal.Decimal(data['bid1'])
  61. }
  62. else:
  63. return {"error": f"MEXC合约({pair_symbol}) API响应中缺少ask1或bid1"}
  64. else:
  65. error_msg = response_data.get('message', '未知MEXC错误或合约不存在')
  66. return {"error": f"MEXC合约({pair_symbol}) API错误: {error_msg}"}
  67. except Exception as e:
  68. return {"error": f"MEXC合约({pair_symbol})请求错误: {e}"}
  69. def get_binance_base_currency_price_usdt(symbol):
  70. url = "https://api.binance.com/api/v3/ticker/price";
  71. params = {'symbol': symbol.replace('_', '')}
  72. try:
  73. response = requests.get(url, params=params, proxies=proxies, timeout=10);
  74. response.raise_for_status();
  75. data = response.json()
  76. if 'price' in data and data.get('symbol') == params['symbol']:
  77. return {"price_base_usdt": decimal.Decimal(data['price'])}
  78. else:
  79. msg = data.get('msg', '未知错误'); return {"error": f"Binance API错误 ({symbol}): {msg}"}
  80. except Exception as e:
  81. return {"error": f"Binance API ({symbol}) 错误: {e}"}
  82. app = Flask(__name__)
  83. log = logging.getLogger('werkzeug');
  84. log.setLevel(logging.ERROR)
  85. MAX_HISTORY_POINTS_PLOTLY = 86400;
  86. REFRESH_INTERVAL_SECONDS = 1
  87. historical_data_points = deque(maxlen=MAX_HISTORY_POINTS_PLOTLY)
  88. TARGET_ASSET_SYMBOL = MEXC_TARGET_CONTRACT_PAIR_USDT.split('_')[0]
  89. BASE_CURRENCY_SYMBOL = BINANCE_BASE_CURRENCY_PAIR_USDT.replace('USDT', '')
  90. latest_values_for_table = {
  91. f"oo_target_vs_base_price": "N/A",
  92. f"mexc_target_vs_base_price_ask1": "N/A", # MEXC Ask1
  93. f"mexc_target_vs_base_price_bid1": "N/A", # MEXC Bid1
  94. f"binance_base_vs_usdt_price": "N/A",
  95. f"diff_oo_vs_mexc_ask1_percentage": "N/A", # Diff OO vs MEXC Ask1
  96. f"diff_oo_vs_mexc_bid1_percentage": "N/A", # Diff OO vs MEXC Bid1
  97. "oo_error": None, "mexc_error": None, "binance_error": None,
  98. "last_updated": "N/A",
  99. "mexc_contract_pair_usdt": MEXC_TARGET_CONTRACT_PAIR_USDT,
  100. "target_asset_symbol_for_display": TARGET_ASSET_SYMBOL,
  101. "base_currency_symbol_for_display": BASE_CURRENCY_SYMBOL
  102. }
  103. data_lock = threading.Lock()
  104. def calculate_percentage_diff(price_a, price_b): # No change
  105. if price_a is not None and price_b is not None and isinstance(price_a, decimal.Decimal) and isinstance(price_b,
  106. decimal.Decimal) and price_b != 0: return (
  107. (price_a - price_b) / price_b) * 100
  108. return None
  109. def convert_usdt_price_to_base_currency_price(price_target_usdt, price_base_usdt): # No change
  110. if price_target_usdt is not None and price_base_usdt is not None and price_base_usdt > 0: return price_target_usdt / price_base_usdt
  111. return None
  112. def update_data_for_plotly_and_table():
  113. global historical_data_points, latest_values_for_table
  114. print(f"数据更新线程 ({TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL})...")
  115. while True:
  116. fetch_time_full = time.strftime("%Y-%m-%d %H:%M:%S");
  117. fetch_time_chart = time.strftime("%H:%M:%S")
  118. oo_data = get_openocean_price_vs_base_currency('eth', IN_TOKEN_ADDRESS_ETH, OUT_TOKEN_ADDRESS_TARGET_ETH,
  119. AMOUNT_TO_QUERY_OPENOCEAN_IN_ETH)
  120. mexc_ticker_usdt_data = get_mexc_contract_ticker_usdt(MEXC_TARGET_CONTRACT_PAIR_USDT) # Get full ticker
  121. binance_base_usdt_data = get_binance_base_currency_price_usdt(BINANCE_BASE_CURRENCY_PAIR_USDT)
  122. oo_target_vs_base_price = oo_data.get("price_base_per_target")
  123. mexc_target_usdt_price_ask1 = mexc_ticker_usdt_data.get("price_target_usdt_ask1")
  124. mexc_target_usdt_price_bid1 = mexc_ticker_usdt_data.get("price_target_usdt_bid1")
  125. base_usdt_price = binance_base_usdt_data.get("price_base_usdt")
  126. oo_err = oo_data.get("error");
  127. mexc_err = mexc_ticker_usdt_data.get("error");
  128. binance_err = binance_base_usdt_data.get("error")
  129. mexc_target_vs_base_price_ask1 = convert_usdt_price_to_base_currency_price(mexc_target_usdt_price_ask1,
  130. base_usdt_price)
  131. mexc_target_vs_base_price_bid1 = convert_usdt_price_to_base_currency_price(mexc_target_usdt_price_bid1,
  132. base_usdt_price)
  133. diff_oo_vs_mexc_ask1_pct = calculate_percentage_diff(oo_target_vs_base_price, mexc_target_vs_base_price_ask1)
  134. diff_oo_vs_mexc_bid1_pct = calculate_percentage_diff(oo_target_vs_base_price, mexc_target_vs_base_price_bid1)
  135. current_point = {
  136. "time": fetch_time_chart,
  137. "oo_target_vs_base": float(oo_target_vs_base_price) if oo_target_vs_base_price else None,
  138. "mexc_target_vs_base_ask1": float(
  139. mexc_target_vs_base_price_ask1) if mexc_target_vs_base_price_ask1 else None,
  140. "mexc_target_vs_base_bid1": float(
  141. mexc_target_vs_base_price_bid1) if mexc_target_vs_base_price_bid1 else None,
  142. "diff_oo_vs_mexc_ask1": float(diff_oo_vs_mexc_ask1_pct) if diff_oo_vs_mexc_ask1_pct is not None else None,
  143. "diff_oo_vs_mexc_bid1": float(diff_oo_vs_mexc_bid1_pct) if diff_oo_vs_mexc_bid1_pct is not None else None,
  144. }
  145. with data_lock:
  146. historical_data_points.append(current_point)
  147. latest_values_for_table[f"oo_target_vs_base_price"] = str(
  148. oo_target_vs_base_price) if oo_target_vs_base_price else "N/A"
  149. latest_values_for_table[f"mexc_target_vs_base_price_ask1"] = str(
  150. mexc_target_vs_base_price_ask1) if mexc_target_vs_base_price_ask1 else "N/A"
  151. latest_values_for_table[f"mexc_target_vs_base_price_bid1"] = str(
  152. mexc_target_vs_base_price_bid1) if mexc_target_vs_base_price_bid1 else "N/A"
  153. latest_values_for_table[f"binance_base_vs_usdt_price"] = str(base_usdt_price) if base_usdt_price else "N/A"
  154. latest_values_for_table[
  155. f"diff_oo_vs_mexc_ask1_percentage"] = f"{diff_oo_vs_mexc_ask1_pct:+.4f}%" if diff_oo_vs_mexc_ask1_pct is not None else "N/A"
  156. latest_values_for_table[
  157. f"diff_oo_vs_mexc_bid1_percentage"] = f"{diff_oo_vs_mexc_bid1_pct:+.4f}%" if diff_oo_vs_mexc_bid1_pct is not None else "N/A"
  158. latest_values_for_table["oo_error"] = oo_err;
  159. latest_values_for_table["mexc_error"] = mexc_err
  160. latest_values_for_table["binance_error"] = binance_err;
  161. latest_values_for_table["last_updated"] = fetch_time_full
  162. ok_oo = 'OK' if oo_target_vs_base_price else 'F';
  163. ok_mexc_ask = 'OK' if mexc_target_vs_base_price_ask1 else 'F'
  164. ok_mexc_bid = 'OK' if mexc_target_vs_base_price_bid1 else 'F';
  165. ok_base = 'OK' if base_usdt_price else 'F'
  166. print(f"{fetch_time_chart} Fetch | OO:{ok_oo} | MEXC Ask1:{ok_mexc_ask} Bid1:{ok_mexc_bid} | Base:{ok_base}")
  167. time.sleep(REFRESH_INTERVAL_SECONDS)
  168. @app.route('/')
  169. def index_plotly():
  170. return render_template('index_plotly_dynamic.html', target_asset=TARGET_ASSET_SYMBOL,
  171. base_asset=BASE_CURRENCY_SYMBOL,
  172. mexc_contract_pair_usdt=MEXC_TARGET_CONTRACT_PAIR_USDT, # Changed from spot to contract
  173. binance_bridge_pair=BINANCE_BASE_CURRENCY_PAIR_USDT,
  174. refresh_interval_ms=REFRESH_INTERVAL_SECONDS * 1000)
  175. @app.route('/table-data')
  176. def get_table_data():
  177. with data_lock:
  178. return jsonify(latest_values_for_table)
  179. @app.route('/plotly-chart-data')
  180. def get_plotly_chart_data():
  181. with data_lock:
  182. points = list(historical_data_points)
  183. if not points: fig = go.Figure(); fig.update_layout(title_text="暂无数据"); empty_json = json.loads(
  184. json.dumps(fig, cls=PlotlyJSONEncoder)); return jsonify(
  185. {"price_chart": empty_json, "diff_chart": empty_json})
  186. times = [p['time'] for p in points]
  187. display_target_asset = latest_values_for_table["target_asset_symbol_for_display"]
  188. display_base_asset = latest_values_for_table["base_currency_symbol_for_display"]
  189. display_mexc_pair_usdt = latest_values_for_table["mexc_contract_pair_usdt"] # Changed
  190. common_xaxis_config = dict(title='时间');
  191. common_legend_config = dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
  192. if len(times) > 1:
  193. common_xaxis_config['range'] = [times[0], times[-1]];
  194. else:
  195. common_xaxis_config['autorange'] = True
  196. fig_prices = go.Figure()
  197. fig_prices.add_trace(go.Scatter(x=times, y=[p['oo_target_vs_base'] for p in points], mode='lines',
  198. name=f'OpenOcean ({display_target_asset}/{display_base_asset})',
  199. line=dict(color='rgb(75, 192, 192)'),
  200. hovertemplate=f'<b>OO</b><br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>'))
  201. fig_prices.add_trace(go.Scatter(x=times, y=[p['mexc_target_vs_base_ask1'] for p in points], mode='lines',
  202. name=f'MEXC Ask1 ({display_target_asset}/{display_base_asset})',
  203. line=dict(color='rgb(255, 99, 132)'),
  204. hovertemplate=f'<b>MEXC Ask1</b><br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>'))
  205. # Add MEXC Bid1 trace
  206. fig_prices.add_trace(go.Scatter(x=times, y=[p['mexc_target_vs_base_bid1'] for p in points], mode='lines',
  207. name=f'MEXC Bid1 ({display_target_asset}/{display_base_asset})',
  208. line=dict(color='rgb(255, 0, 0)', dash='dash'),
  209. hovertemplate=f'<b>MEXC Bid1</b><br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>')) # Different color/style for Bid1
  210. fig_prices.update_layout(title_text=f'{display_target_asset}/{display_base_asset} 价格历史',
  211. xaxis=common_xaxis_config.copy(),
  212. yaxis_title=f'价格 (1 {display_target_asset} = X {display_base_asset})',
  213. legend_title_text='平台',
  214. legend=common_legend_config.copy(), hovermode='x unified',
  215. margin=dict(l=70, r=30, t=80, b=50))
  216. fig_diffs = go.Figure() # Diff chart may need adjustment if you want to show diffs against bid1 too
  217. fig_diffs.add_trace(
  218. go.Scatter(x=times, y=[p['diff_oo_vs_mexc_ask1'] for p in points], mode='lines', name=f'OO vs MEXC Ask1',
  219. line=dict(color='rgb(255, 159, 64)'),
  220. hovertemplate=f'<b>OO vs MEXC Ask1</b><br>Diff: %{{y:+.4f}}%<extra></extra>'))
  221. # Optionally, add diff for OO vs MEXC Bid1
  222. fig_diffs.add_trace(
  223. go.Scatter(x=times, y=[p['diff_oo_vs_mexc_bid1'] for p in points], mode='lines', name=f'OO vs MEXC Bid1',
  224. line=dict(color='rgb(200, 100, 50)', dash='dot'),
  225. hovertemplate=f'<b>OO vs MEXC Bid1</b><br>Diff: %{{y:+.4f}}%<extra></extra>'))
  226. fig_diffs.update_layout(title_text=f'{display_target_asset}/{display_base_asset} 价差百分比历史',
  227. xaxis=common_xaxis_config.copy(),
  228. yaxis_title='价差 (%)', legend_title_text='对比', legend=common_legend_config.copy(),
  229. yaxis_zeroline=True, hovermode='x unified', margin=dict(l=70, r=30, t=80, b=50))
  230. combined_figure_data = {"price_chart": json.loads(json.dumps(fig_prices, cls=PlotlyJSONEncoder)),
  231. "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder))};
  232. return jsonify(combined_figure_data)
  233. if __name__ == "__main__": # No change in __main__ needed for this specific request
  234. print("应用启动...");
  235. print(f"目标资产: {TARGET_ASSET_SYMBOL}");
  236. print(f"基础货币 (用于计价): {BASE_CURRENCY_SYMBOL}")
  237. print(
  238. f"OpenOcean: {TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL} (通过 {IN_TOKEN_ADDRESS_ETH[-6:]}...{BASE_CURRENCY_SYMBOL} / {OUT_TOKEN_ADDRESS_TARGET_ETH[-6:]}...{TARGET_ASSET_SYMBOL} on ETH)")
  239. print(
  240. f"MEXC 合约: {MEXC_TARGET_CONTRACT_PAIR_USDT} (将转换为 {TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL})") # Changed spot to contract
  241. print(f"币安转换汇率: {BINANCE_BASE_CURRENCY_PAIR_USDT}")
  242. data_thread = threading.Thread(target=update_data_for_plotly_and_table, daemon=True);
  243. data_thread.start()
  244. print(f"Flask 服务将在 http://0.0.0.0:5000 上运行 (刷新间隔: {REFRESH_INTERVAL_SECONDS}s)");
  245. app.run(debug=False, host='0.0.0.0', port=5000, use_reloader=False)