price_checker.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import requests
  2. import decimal
  3. import time
  4. import threading
  5. import json
  6. from flask import Flask, render_template, jsonify # jsonify 仍然有用,但/data可能直接返回Plotly JSON字符串
  7. from collections import deque
  8. import logging
  9. import plotly # 引入 Plotly
  10. import plotly.graph_objects as go
  11. from plotly.utils import PlotlyJSONEncoder # 用于将Plotly图表序列化为JSON
  12. # --- 配置部分 (与之前相同) ---
  13. IN_TOKEN_ADDRESS_BSC = '0x55d398326f99059ff775485246999027b3197955'
  14. OUT_TOKEN_ADDRESS_BSC = '0x3AeE7602b612de36088F3ffEd8c8f10E86EbF2bF' # out token
  15. AMOUNT_TO_QUERY_HUMAN = decimal.Decimal('1000')
  16. GATEIO_SPOT_PAIR = 'BANK_USDT'
  17. GATEIO_FUTURES_CONTRACT = 'BANK_USDT'
  18. proxies = None
  19. decimal.getcontext().prec = 36
  20. # --- 价格获取函数 (与之前相同, 省略以保持简洁) ---
  21. def get_openocean_price_bsc(in_token_addr, out_token_addr, human_amount_in_decimal_for_request, gas_price=3):
  22. chain = 'bsc';
  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': gas_price}
  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, h_out = atomic_in / (10 ** in_dec), atomic_out / (10 ** out_dec)
  38. if h_out == 0: return {"error": "OO输出为0"}
  39. return {"price_usdt_per_out_token": h_in / h_out}
  40. else:
  41. return {
  42. "error": f"OO API错误 - Code:{data.get('code', 'N/A')}, Msg:{data.get('msg', data.get('message', 'N/A')) if isinstance(data, dict) else '格式错误'}"}
  43. except Exception as e:
  44. return {"error": f"OO请求错误: {e}"}
  45. def get_gateio_spot_price(pair_symbol):
  46. url = f'https://api.gateio.ws/api/v4/spot/tickers';
  47. params = {'currency_pair': pair_symbol}
  48. try:
  49. r = requests.get(url, params=params, proxies=proxies, timeout=10);
  50. r.raise_for_status();
  51. data = r.json()
  52. if isinstance(data, list) and data:
  53. td = data[0]
  54. if td.get('currency_pair') == pair_symbol and td.get('last'):
  55. return {"price_base_in_quote": decimal.Decimal(td['last'])}
  56. else:
  57. return {"error": f"Gate现货数据不匹配(Req:{pair_symbol},Res:{td.get('currency_pair')})或无价格"}
  58. elif isinstance(data, dict) and data.get('label'):
  59. return {"error": f"Gate现货API错误: {data.get('label')}-{data.get('message', '')}"}
  60. else:
  61. return {"error": f"Gate现货API数据格式错误或未找到 {pair_symbol}"}
  62. except Exception as e:
  63. return {"error": f"Gate现货请求错误: {e}"}
  64. def get_gateio_futures_price(contract_symbol, settle_currency='usdt'):
  65. url = f'https://api.gateio.ws/api/v4/futures/{settle_currency}/tickers';
  66. params = {'contract': contract_symbol}
  67. try:
  68. r = requests.get(url, params=params, proxies=proxies, timeout=10);
  69. r.raise_for_status();
  70. data = r.json()
  71. if isinstance(data, list) and data:
  72. td = data[0]
  73. if td.get('contract') == contract_symbol and td.get('last'):
  74. return {"price_settle_per_base_asset": decimal.Decimal(td['last'])}
  75. else:
  76. return {"error": f"Gate期货数据不匹配(Req:{contract_symbol},Res:{td.get('contract')})或无价格"}
  77. elif isinstance(data, dict) and data.get('label'):
  78. return {"error": f"Gate期货API错误: {data.get('label')}-{data.get('message', '')}"}
  79. else:
  80. return {"error": f"Gate期货API数据格式错误或未找到 {contract_symbol}"}
  81. except Exception as e:
  82. return {"error": f"Gate期货请求错误: {e}"}
  83. app = Flask(__name__)
  84. log = logging.getLogger('werkzeug');
  85. log.setLevel(logging.ERROR)
  86. MAX_HISTORY_POINTS_PLOTLY = 300 # Plotly 图表显示的点数
  87. REFRESH_INTERVAL_SECONDS = 1 # 后端数据更新频率
  88. # 存储原始数据点,用于 Plotly 生成图表
  89. historical_data_points = deque(maxlen=MAX_HISTORY_POINTS_PLOTLY)
  90. # 存储最新的数值,用于表格展示 (这个结构可以保留)
  91. latest_values_for_table = {
  92. "oo_price": "N/A", "gate_spot_price": "N/A", "gate_futures_price": "N/A",
  93. "diff_oo_vs_spot_percentage": "N/A", "diff_oo_vs_futures_percentage": "N/A",
  94. "diff_spot_vs_futures_percentage": "N/A",
  95. "oo_error": None, "gate_spot_error": None, "gate_futures_error": None,
  96. "last_updated": "N/A",
  97. "gate_spot_pair_name": GATEIO_SPOT_PAIR,
  98. "gate_futures_contract_name": GATEIO_FUTURES_CONTRACT,
  99. "target_asset_symbol": GATEIO_SPOT_PAIR.split('_')[0]
  100. }
  101. data_lock = threading.Lock() # 保护共享数据
  102. def calculate_percentage_diff(price_a, price_b):
  103. if price_a is not None and price_b is not None and isinstance(price_a, decimal.Decimal) and isinstance(price_b,
  104. decimal.Decimal) and price_b > 0:
  105. return ((price_a - price_b) / price_b) * 100
  106. return None
  107. def update_data_for_plotly_and_table():
  108. global historical_data_points, latest_values_for_table
  109. print("数据更新线程启动...")
  110. while True:
  111. fetch_time_full = time.strftime("%Y-%m-%d %H:%M:%S")
  112. fetch_time_chart = time.strftime("%H:%M:%S") # 或者使用 time.time() 获取更精确的unix时间戳
  113. oo_data = get_openocean_price_bsc(IN_TOKEN_ADDRESS_BSC, OUT_TOKEN_ADDRESS_BSC, AMOUNT_TO_QUERY_HUMAN)
  114. spot_data = get_gateio_spot_price(GATEIO_SPOT_PAIR)
  115. futures_data = get_gateio_futures_price(GATEIO_FUTURES_CONTRACT)
  116. oo_price = oo_data.get("price_usdt_per_out_token") # decimal or None
  117. spot_price = spot_data.get("price_base_in_quote") # decimal or None
  118. futures_price = futures_data.get("price_settle_per_base_asset") # decimal or None
  119. oo_err = oo_data.get("error")
  120. spot_err = spot_data.get("error")
  121. futures_err = futures_data.get("error")
  122. diff_oo_spot_pct = calculate_percentage_diff(oo_price, spot_price)
  123. diff_oo_futures_pct = calculate_percentage_diff(oo_price, futures_price)
  124. diff_spot_futures_pct = calculate_percentage_diff(spot_price, futures_price)
  125. current_point = {
  126. "time": fetch_time_chart, # 使用时间字符串作为X轴,Plotly可以处理
  127. "oo_price": float(oo_price) if oo_price else None,
  128. "spot_price": float(spot_price) if spot_price else None,
  129. "futures_price": float(futures_price) if futures_price else None,
  130. "diff_oo_spot": float(diff_oo_spot_pct) if diff_oo_spot_pct is not None else None,
  131. "diff_oo_futures": float(diff_oo_futures_pct) if diff_oo_futures_pct is not None else None,
  132. "diff_spot_futures": float(diff_spot_futures_pct) if diff_spot_futures_pct is not None else None,
  133. }
  134. with data_lock:
  135. historical_data_points.append(current_point)
  136. latest_values_for_table["oo_price"] = str(oo_price) if oo_price else "N/A"
  137. latest_values_for_table["gate_spot_price"] = str(spot_price) if spot_price else "N/A"
  138. latest_values_for_table["gate_futures_price"] = str(futures_price) if futures_price else "N/A"
  139. latest_values_for_table[
  140. "diff_oo_vs_spot_percentage"] = f"{diff_oo_spot_pct:+.4f}%" if diff_oo_spot_pct is not None else "N/A"
  141. latest_values_for_table[
  142. "diff_oo_vs_futures_percentage"] = f"{diff_oo_futures_pct:+.4f}%" if diff_oo_futures_pct is not None else "N/A"
  143. latest_values_for_table[
  144. "diff_spot_vs_futures_percentage"] = f"{diff_spot_futures_pct:+.4f}%" if diff_spot_futures_pct is not None else "N/A"
  145. latest_values_for_table["oo_error"] = oo_err
  146. latest_values_for_table["gate_spot_error"] = spot_err
  147. latest_values_for_table["gate_futures_error"] = futures_err
  148. latest_values_for_table["last_updated"] = fetch_time_full
  149. # Log a summary, not too verbose
  150. # print(f"{fetch_time_chart} Update | "
  151. # f"OO:{'OK' if oo_price else 'Fail'} "
  152. # f"GS:{'OK' if spot_price else 'Fail'} "
  153. # f"GF:{'OK' if futures_price else 'Fail'}")
  154. time.sleep(REFRESH_INTERVAL_SECONDS)
  155. @app.route('/')
  156. def index_plotly():
  157. # 将配置传递给模板,用于标题等
  158. target_asset = GATEIO_SPOT_PAIR.split('_')[0]
  159. return render_template('index_plotly.html',
  160. target_asset=target_asset,
  161. spot_pair=GATEIO_SPOT_PAIR,
  162. futures_contract=GATEIO_FUTURES_CONTRACT,
  163. refresh_interval_ms=REFRESH_INTERVAL_SECONDS * 1000)
  164. @app.route('/table-data') # 新增一个专门用于表格数据的API
  165. def get_table_data():
  166. with data_lock:
  167. return jsonify(latest_values_for_table)
  168. @app.route('/plotly-chart-data') # 用于Plotly图表的JSON数据
  169. def get_plotly_chart_data():
  170. with data_lock:
  171. # 从 deque 转换为列表,方便 Plotly 处理
  172. points = list(historical_data_points)
  173. if not points: # 如果没有数据点,返回空图表结构或错误提示
  174. fig = go.Figure()
  175. fig.update_layout(title_text="暂无数据", xaxis_title="时间", yaxis_title="价格/百分比")
  176. return json.dumps(fig, cls=PlotlyJSONEncoder)
  177. times = [p['time'] for p in points]
  178. # --- 创建价格图 (主图) ---
  179. fig_prices = go.Figure()
  180. # 在这里为每个 trace 添加 hovertemplate
  181. fig_prices.add_trace(go.Scatter(
  182. x=times, y=[p['oo_price'] for p in points], mode='lines', name='OpenOcean',
  183. line=dict(color='rgb(75, 192, 192)'),
  184. hovertemplate='<b>OpenOcean</b><br>Time: %{x}<br>Price: %{y:.6f}<extra></extra>' # .6f 表示6位小数
  185. ))
  186. fig_prices.add_trace(go.Scatter(
  187. x=times, y=[p['spot_price'] for p in points], mode='lines', name=f'Gate Spot ({GATEIO_SPOT_PAIR})',
  188. line=dict(color='rgb(255, 99, 132)'),
  189. hovertemplate=f'<b>Gate Spot ({GATEIO_SPOT_PAIR})</b><br>Time: %{{x}}<br>Price: %{{y:.6f}}<extra></extra>'
  190. ))
  191. fig_prices.add_trace(go.Scatter(
  192. x=times, y=[p['futures_price'] for p in points], mode='lines',
  193. name=f'Gate Futures ({GATEIO_FUTURES_CONTRACT})',
  194. line=dict(color='rgb(54, 162, 235)'),
  195. hovertemplate=f'<b>Gate Futures ({GATEIO_FUTURES_CONTRACT})</b><br>Time: %{{x}}<br>Price: %{{y:.6f}}<extra></extra>'
  196. ))
  197. target_asset = GATEIO_SPOT_PAIR.split('_')[0]
  198. fig_prices.update_layout(
  199. title_text=f'{target_asset}/USDT 价格历史',
  200. xaxis_title='时间',
  201. yaxis_title=f'价格 ({target_asset}/USDT)',
  202. legend_title_text='平台',
  203. hovermode='x unified' # 或者 'x', 'closest'
  204. )
  205. # --- 创建价差图 (副图) ---
  206. fig_diffs = go.Figure()
  207. # 在这里为每个 trace 添加 hovertemplate
  208. fig_diffs.add_trace(go.Scatter(
  209. x=times, y=[p['diff_oo_spot'] for p in points], mode='lines', name=f'OO vs Spot ({GATEIO_SPOT_PAIR})',
  210. line=dict(color='rgb(255, 159, 64)'),
  211. hovertemplate=f'<b>OO vs Spot ({GATEIO_SPOT_PAIR})</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'
  212. # :+.4f 表示带符号4位小数
  213. ))
  214. fig_diffs.add_trace(go.Scatter(
  215. x=times, y=[p['diff_oo_futures'] for p in points], mode='lines',
  216. name=f'OO vs Futures ({GATEIO_FUTURES_CONTRACT})',
  217. line=dict(color='rgb(153, 102, 255)'),
  218. hovertemplate=f'<b>OO vs Futures ({GATEIO_FUTURES_CONTRACT})</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'
  219. ))
  220. fig_diffs.add_trace(go.Scatter(
  221. x=times, y=[p['diff_spot_futures'] for p in points], mode='lines',
  222. name=f'Spot ({GATEIO_SPOT_PAIR}) vs Futures ({GATEIO_FUTURES_CONTRACT})',
  223. line=dict(color='rgb(75, 192, 75)'),
  224. hovertemplate=f'<b>Spot ({GATEIO_SPOT_PAIR}) vs Futures ({GATEIO_FUTURES_CONTRACT})</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'
  225. ))
  226. fig_diffs.update_layout(
  227. title_text='价差百分比历史',
  228. xaxis_title='时间',
  229. yaxis_title='价差 (%)',
  230. legend_title_text='对比',
  231. yaxis_zeroline=True,
  232. hovermode='x unified' # 统一显示同一X轴下所有trace的值
  233. )
  234. # Plotly 不直接支持通过 fig.to_json() 合并具有完全独立Y轴的子图到一个figure中并保持良好交互性
  235. # 最简单的方式是前端请求两个独立的图表JSON,然后分别渲染。
  236. # 或者,我们可以使用 make_subplots,但共享X轴是关键。
  237. # 为了简化前端,我们这里返回两个独立的Figure的JSON。前端需要能处理这个结构。
  238. # 或者,更常见的是,前端请求两个不同的API端点来获取图表数据。
  239. # 这里,我们将返回一个包含两个图表定义的字典。
  240. combined_figure_data = {
  241. "price_chart": json.loads(json.dumps(fig_prices, cls=PlotlyJSONEncoder)), # 确保可JSON序列化
  242. "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder))
  243. }
  244. return jsonify(combined_figure_data) # jsonify 会自动处理JSON序列化
  245. if __name__ == "__main__":
  246. print("应用启动...")
  247. print(f"监控 OpenOcean (BSC代币: ...{OUT_TOKEN_ADDRESS_BSC[-6:]}) vs USDT")
  248. print(f"Gate.io 现货: {GATEIO_SPOT_PAIR}")
  249. print(f"Gate.io 期货: {GATEIO_FUTURES_CONTRACT}")
  250. # ... (启动时的配置检查可以保留) ...
  251. spot_base = GATEIO_SPOT_PAIR.split('_')[0].upper()
  252. futures_base = GATEIO_FUTURES_CONTRACT.split('_')[0].upper()
  253. oo_asset_placeholder = "CAT" if "0x6894CDe390a3f51155ea41Ed24a33A4827d3063D" in OUT_TOKEN_ADDRESS_BSC else "TOKEN"
  254. if oo_asset_placeholder != spot_base: print(
  255. f"[警告] OO资产('{oo_asset_placeholder}')与Gate现货基础资产('{spot_base}')可能不匹配!")
  256. if oo_asset_placeholder != futures_base: print(
  257. f"[警告] OO资产('{oo_asset_placeholder}')与Gate期货基础资产('{futures_base}')可能不匹配!")
  258. if spot_base != futures_base: print(
  259. f"[警告] Gate现货基础资产('{spot_base}')与期货基础资产('{futures_base}')不匹配!")
  260. data_thread = threading.Thread(target=update_data_for_plotly_and_table, daemon=True)
  261. data_thread.start()
  262. print(f"Flask 服务将在 http://0.0.0.0:5000 上运行 (刷新间隔: {REFRESH_INTERVAL_SECONDS}s)")
  263. app.run(debug=False, host='0.0.0.0', port=5000, use_reloader=False)