price_checker.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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_SOLANA = 'So11111111111111111111111111111111111111112'
  14. OUT_TOKEN_ADDRESS_SOLANA = 'ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY'
  15. AMOUNT_TO_QUERY_OPENOCEAN_IN_SOL = decimal.Decimal('10')
  16. GATEIO_SPOT_PAIR_RFC_USDT = 'MOODENG_USDT'
  17. BINANCE_SOL_USDT_PAIR = 'SOLUSDT'
  18. proxies = None
  19. decimal.getcontext().prec = 36
  20. # --- 价格获取函数 (与上一版相同,此处省略以保持简洁) ---
  21. def get_openocean_price_solana(in_token_addr, out_token_addr, human_amount_in_decimal_for_request):
  22. chain = 'solana';
  23. url = f'https://open-api.openocean.finance/v4/{chain}/quote'
  24. params = {'inTokenAddress': in_token_addr, 'outTokenAddress': out_token_addr,
  25. 'amount': str(human_amount_in_decimal_for_request), 'gasPrice': '0.000005'}
  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, atomic_out = decimal.Decimal(d['inAmount']), decimal.Decimal(d['outAmount'])
  37. h_in_sol = atomic_in / (10 ** in_dec);
  38. h_out_rfc = atomic_out / (10 ** out_dec)
  39. if h_out_rfc == 0: return {"error": "OO输出RFC为0"}
  40. return {"price_sol_per_rfc": h_in_sol / h_out_rfc}
  41. else:
  42. return {
  43. "error": f"OO API错误 - 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(Solana)请求错误: {e}"}
  46. def get_gateio_spot_price_usdt(pair_symbol):
  47. url = f'https://api.gateio.ws/api/v4/spot/tickers';
  48. params = {'currency_pair': pair_symbol}
  49. try:
  50. r = requests.get(url, params=params, proxies=proxies, timeout=10);
  51. r.raise_for_status();
  52. data = r.json()
  53. if isinstance(data, list) and data:
  54. td = data[0]
  55. if td.get('currency_pair') == pair_symbol and td.get('lowest_ask'):
  56. return {"price_rfc_usdt": decimal.Decimal(td['lowest_ask'])}
  57. else:
  58. return {"error": f"Gate现货({pair_symbol})数据不匹配或无价格"}
  59. elif isinstance(data, dict) and data.get('label'):
  60. return {"error": f"Gate现货API错误: {data['label']}-{data.get('message', '')}"}
  61. else:
  62. return {"error": f"Gate现货API({pair_symbol})数据格式错误或未找到"}
  63. except Exception as e:
  64. return {"error": f"Gate现货({pair_symbol})请求错误: {e}"}
  65. def get_binance_spot_price_usdt(symbol):
  66. url = "https://api.binance.com/api/v3/ticker/price";
  67. params = {'symbol': symbol}
  68. try:
  69. response = requests.get(url, params=params, proxies=proxies, timeout=10);
  70. response.raise_for_status();
  71. data = response.json()
  72. if 'price' in data and data.get('symbol') == symbol:
  73. return {"price_sol_usdt": decimal.Decimal(data['price'])}
  74. else:
  75. msg = data.get('msg', '未知错误'); return {"error": f"Binance API错误 ({symbol}): {msg}"}
  76. except requests.exceptions.RequestException as e:
  77. return {"error": f"Binance API ({symbol}) 请求失败: {e}"}
  78. except Exception as e:
  79. return {"error": f"Binance API ({symbol}) 意外错误: {e}"}
  80. app = Flask(__name__)
  81. log = logging.getLogger('werkzeug');
  82. log.setLevel(logging.ERROR)
  83. MAX_HISTORY_POINTS_PLOTLY = 86400
  84. REFRESH_INTERVAL_SECONDS = 1
  85. historical_data_points = deque(maxlen=MAX_HISTORY_POINTS_PLOTLY)
  86. latest_values_for_table = {
  87. "oo_rfc_sol_price": "N/A", "gate_spot_rfc_sol_price": "N/A", "sol_usdt_price_binance": "N/A",
  88. "diff_oo_vs_spot_rfc_sol_percentage": "N/A",
  89. "oo_error": None, "gate_spot_error": None, "binance_sol_error": None,
  90. "last_updated": "N/A", "gate_spot_pair_name_usdt": GATEIO_SPOT_PAIR_RFC_USDT,
  91. "target_asset_symbol": GATEIO_SPOT_PAIR_RFC_USDT.split('_')[0]
  92. }
  93. data_lock = threading.Lock()
  94. def calculate_percentage_diff(price_a, price_b):
  95. if price_a is not None and price_b is not None and isinstance(price_a, decimal.Decimal) and isinstance(price_b,
  96. decimal.Decimal) and price_b != 0:
  97. return ((price_a - price_b) / price_b) * 100
  98. return None
  99. def convert_usdt_price_to_sol_price(price_asset_usdt, price_sol_usdt):
  100. if price_asset_usdt is not None and price_sol_usdt is not None and price_sol_usdt > 0:
  101. return price_asset_usdt / price_sol_usdt
  102. return None
  103. def update_data_for_plotly_and_table():
  104. global historical_data_points, latest_values_for_table
  105. print("数据更新线程启动 (仅现货)...")
  106. while True:
  107. fetch_time_full = time.strftime("%Y-%m-%d %H:%M:%S");
  108. fetch_time_chart = time.strftime("%H:%M:%S")
  109. oo_data = get_openocean_price_solana(IN_TOKEN_ADDRESS_SOLANA, OUT_TOKEN_ADDRESS_SOLANA,
  110. AMOUNT_TO_QUERY_OPENOCEAN_IN_SOL)
  111. spot_usdt_data = get_gateio_spot_price_usdt(GATEIO_SPOT_PAIR_RFC_USDT)
  112. binance_sol_usdt_data = get_binance_spot_price_usdt(BINANCE_SOL_USDT_PAIR)
  113. oo_rfc_sol_price = oo_data.get("price_sol_per_rfc");
  114. spot_rfc_usdt_price = spot_usdt_data.get("price_rfc_usdt")
  115. sol_usdt_price = binance_sol_usdt_data.get("price_sol_usdt")
  116. oo_err = oo_data.get("error");
  117. spot_err = spot_usdt_data.get("error");
  118. binance_err = binance_sol_usdt_data.get("error")
  119. spot_rfc_sol_price = convert_usdt_price_to_sol_price(spot_rfc_usdt_price, sol_usdt_price)
  120. diff_oo_spot_pct = calculate_percentage_diff(oo_rfc_sol_price, spot_rfc_sol_price)
  121. current_point = {
  122. "time": fetch_time_chart,
  123. "oo_rfc_sol": float(oo_rfc_sol_price) if oo_rfc_sol_price else None,
  124. "spot_rfc_sol": float(spot_rfc_sol_price) if spot_rfc_sol_price else None,
  125. "diff_oo_spot_rfc_sol": float(diff_oo_spot_pct) if diff_oo_spot_pct is not None else None,
  126. }
  127. with data_lock:
  128. historical_data_points.append(current_point)
  129. latest_values_for_table["oo_rfc_sol_price"] = str(oo_rfc_sol_price) if oo_rfc_sol_price else "N/A"
  130. latest_values_for_table["gate_spot_rfc_sol_price"] = str(
  131. spot_rfc_sol_price) if spot_rfc_sol_price else "N/A"
  132. latest_values_for_table["sol_usdt_price_binance"] = str(sol_usdt_price) if sol_usdt_price else "N/A"
  133. latest_values_for_table[
  134. "diff_oo_vs_spot_rfc_sol_percentage"] = f"{diff_oo_spot_pct:+.4f}%" if diff_oo_spot_pct is not None else "N/A"
  135. latest_values_for_table["oo_error"] = oo_err;
  136. latest_values_for_table["gate_spot_error"] = spot_err
  137. latest_values_for_table["binance_sol_error"] = binance_err;
  138. latest_values_for_table["last_updated"] = fetch_time_full
  139. print(
  140. f"{fetch_time_chart} Fetch | OO_RFC/SOL:{'OK' if oo_rfc_sol_price else 'F'} | Spot_RFC/SOL:{'OK' if spot_rfc_sol_price else 'F'} | SOL/USDT:{'OK' if sol_usdt_price else 'F'}")
  141. time.sleep(REFRESH_INTERVAL_SECONDS)
  142. @app.route('/')
  143. def index_plotly():
  144. target_asset = latest_values_for_table["target_asset_symbol"]
  145. return render_template('index_plotly_sol_spot_only.html', target_asset=target_asset,
  146. spot_pair_usdt=GATEIO_SPOT_PAIR_RFC_USDT, sol_usdt_pair_binance=BINANCE_SOL_USDT_PAIR,
  147. refresh_interval_ms=REFRESH_INTERVAL_SECONDS * 1000)
  148. @app.route('/table-data')
  149. def get_table_data():
  150. with data_lock: return jsonify(latest_values_for_table)
  151. @app.route('/plotly-chart-data')
  152. def get_plotly_chart_data():
  153. with data_lock:
  154. points = list(historical_data_points)
  155. if not points:
  156. fig = go.Figure();
  157. fig.update_layout(title_text="暂无数据")
  158. empty_json = json.loads(json.dumps(fig, cls=PlotlyJSONEncoder))
  159. return jsonify({"price_chart": empty_json, "diff_chart": empty_json})
  160. times = [p['time'] for p in points]
  161. target_asset_symbol = latest_values_for_table["target_asset_symbol"]
  162. common_xaxis_config = dict(title='时间')
  163. if len(times) > 1:
  164. common_xaxis_config['range'] = [times[0], times[-1]]
  165. else:
  166. common_xaxis_config['autorange'] = True
  167. common_legend_config = dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
  168. fig_prices = go.Figure()
  169. fig_prices.add_trace(
  170. go.Scatter(x=times, y=[p['oo_rfc_sol'] for p in points], mode='lines', name='OpenOcean (RFC/SOL)',
  171. line=dict(color='rgb(75, 192, 192)'),
  172. hovertemplate=f'<b>OpenOcean (RFC/SOL)</b><br>Time: %{{x}}<br>Price: %{{y:.8f}} SOL<extra></extra>'))
  173. fig_prices.add_trace(go.Scatter(x=times, y=[p['spot_rfc_sol'] for p in points], mode='lines',
  174. name=f'Gate Spot ({GATEIO_SPOT_PAIR_RFC_USDT} → RFC/SOL)',
  175. line=dict(color='rgb(255, 99, 132)'),
  176. hovertemplate=f'<b>Gate Spot (RFC/SOL)</b><br>Time: %{{x}}<br>Price: %{{y:.8f}} SOL<extra></extra>'))
  177. fig_prices.update_layout(title_text=f'{target_asset_symbol}/SOL 价格历史', xaxis=common_xaxis_config.copy(),
  178. yaxis_title=f'价格 (1 {target_asset_symbol} = X SOL)', legend_title_text='平台',
  179. legend=common_legend_config.copy(), hovermode='x unified',
  180. margin=dict(l=70, r=30, t=80, b=50))
  181. fig_diffs = go.Figure()
  182. fig_diffs.add_trace(go.Scatter(x=times, y=[p['diff_oo_spot_rfc_sol'] for p in points], mode='lines',
  183. name=f'OO vs Spot (RFC/SOL)', line=dict(color='rgb(255, 159, 64)'),
  184. hovertemplate=f'<b>OO vs Spot (RFC/SOL)</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'))
  185. fig_diffs.update_layout(title_text=f'{target_asset_symbol}/SOL 价差百分比历史 (OO vs Spot)',
  186. xaxis=common_xaxis_config.copy(),
  187. yaxis_title='价差 (%)',
  188. legend=common_legend_config.copy(),
  189. # Applying to diff chart as well, or set showlegend=False
  190. # showlegend=False, # Uncomment this if you don't want legend on diff chart
  191. yaxis_zeroline=True, hovermode='x unified', margin=dict(l=70, r=30, t=80, b=50))
  192. combined_figure_data = {"price_chart": json.loads(json.dumps(fig_prices, cls=PlotlyJSONEncoder)),
  193. "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder))}
  194. return jsonify(combined_figure_data)
  195. if __name__ == "__main__":
  196. print("应用启动...")
  197. asset_symbol = GATEIO_SPOT_PAIR_RFC_USDT.split('_')[0]
  198. print(f"目标资产: {asset_symbol}")
  199. print(
  200. f"OpenOcean: {asset_symbol}/SOL (通过 {IN_TOKEN_ADDRESS_SOLANA[-6:]}...SOL / {OUT_TOKEN_ADDRESS_SOLANA[-6:]}...{asset_symbol})")
  201. print(f"Gate.io 现货: {GATEIO_SPOT_PAIR_RFC_USDT} (将转换为 {asset_symbol}/SOL)")
  202. print(f"币安转换汇率: {BINANCE_SOL_USDT_PAIR}")
  203. data_thread = threading.Thread(target=update_data_for_plotly_and_table, daemon=True);
  204. data_thread.start()
  205. print(f"Flask 服务将在 http://0.0.0.0:5000 上运行 (刷新间隔: {REFRESH_INTERVAL_SECONDS}s)")
  206. app.run(debug=False, host='0.0.0.0', port=5000, use_reloader=False)