price_checker.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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. # --- 配置部分 ---
  10. # OpenOcean
  11. IN_TOKEN_ADDRESS_BSC = '0x8F0528cE5eF7B51152A59745bEfDD91D97091d2F' # USDT on BSC
  12. OUT_TOKEN_ADDRESS_BSC = '0x6894CDe390a3f51155ea41Ed24a33A4827d3063D' # CAT on BSC (示例)
  13. AMOUNT_TO_QUERY_HUMAN = decimal.Decimal('1000') # 查询的USDT数量
  14. # Gate.io 现货交易对
  15. GATEIO_SPOT_PAIR = 'ALPACA_USDT' # 示例, 请确保存在且与OUT_TOKEN_ADDRESS_BSC对应
  16. # Gate.io USDT 结算永续合约名称
  17. GATEIO_FUTURES_CONTRACT = 'ALPACA_USDT' # 示例, 请确保存在且与OUT_TOKEN_ADDRESS_BSC对应
  18. # 代理配置
  19. proxies = None
  20. # PROXY_HOST = '127.0.0.1'
  21. # PROXY_PORT = '7890'
  22. # proxies = {
  23. # 'http': f'http://{PROXY_HOST}:{PROXY_PORT}',
  24. # 'https': f'http://{PROXY_HOST}:{PROXY_PORT}',
  25. # }
  26. decimal.getcontext().prec = 36
  27. # --- 价格获取函数 ---
  28. def get_openocean_price_bsc(in_token_addr, out_token_addr, human_amount_in_decimal_for_request, gas_price=3):
  29. # (代码与之前相同, 返回 {"price_usdt_per_out_token": calculated_price} 或 {"error": ...})
  30. chain = 'bsc'
  31. url = f'https://open-api.openocean.finance/v4/{chain}/quote'
  32. params = {
  33. 'inTokenAddress': in_token_addr,
  34. 'outTokenAddress': out_token_addr,
  35. 'amount': str(human_amount_in_decimal_for_request),
  36. 'gasPrice': gas_price,
  37. }
  38. try:
  39. response = requests.get(url, params=params, proxies=proxies, timeout=10)
  40. response.raise_for_status()
  41. data = response.json()
  42. if data.get('code') == 200 and data.get('data'):
  43. api_data = data['data']
  44. required_keys = ['inToken', 'outToken', 'inAmount', 'outAmount']
  45. if not all(key in api_data for key in required_keys) or \
  46. api_data['inToken'].get('decimals') is None or \
  47. api_data['outToken'].get('decimals') is None:
  48. return {"error": "OO API响应中缺少必要的数据"}
  49. in_token_decimals = int(api_data['inToken']['decimals'])
  50. out_token_decimals = int(api_data['outToken']['decimals'])
  51. atomic_in_amount = decimal.Decimal(api_data['inAmount'])
  52. atomic_out_amount = decimal.Decimal(api_data['outAmount'])
  53. human_in_amount_used = atomic_in_amount / (decimal.Decimal('10') ** in_token_decimals)
  54. human_out_amount_received = atomic_out_amount / (decimal.Decimal('10') ** out_token_decimals)
  55. if human_out_amount_received == 0: return {"error": "OO 计算的输出数量为零"}
  56. calculated_price = human_in_amount_used / human_out_amount_received
  57. return {"price_usdt_per_out_token": calculated_price}
  58. else:
  59. error_message = data.get('message', 'N/A') if data else '无响应数据'
  60. error_code = data.get('code', 'N/A') if data else 'N/A'
  61. return {"error": f"OO API 错误 - Code: {error_code}, Msg: {error_message}"}
  62. except requests.exceptions.RequestException as e:
  63. return {"error": f"OO请求失败: {e}"}
  64. except Exception as e:
  65. return {"error": f"OO意外错误: {e}"}
  66. def get_gateio_spot_price(pair_symbol):
  67. # (代码与之前相同, 返回 {"price_base_in_quote": decimal.Decimal(last_price_str)} 或 {"error": ...})
  68. url = f'https://api.gateio.ws/api/v4/spot/tickers'
  69. params = {'currency_pair': pair_symbol}
  70. try:
  71. response = requests.get(url, params=params, proxies=proxies, timeout=10)
  72. response.raise_for_status()
  73. data = response.json()
  74. if isinstance(data, list) and len(data) > 0:
  75. ticker_data = data[0]
  76. if ticker_data.get('currency_pair') == pair_symbol:
  77. last_price_str = ticker_data.get('last')
  78. if last_price_str:
  79. return {"price_base_in_quote": decimal.Decimal(last_price_str)}
  80. else:
  81. return {"error": "Gate现货未找到last price"}
  82. else:
  83. return {"error": f"Gate现货交易对不匹配"}
  84. else:
  85. error_msg = "未知错误或交易对不存在"
  86. if isinstance(data, dict) and data.get('label'):
  87. error_msg = f"{data.get('label')}: {data.get('message', '')}"
  88. return {"error": f"Gate现货 API 数据格式错误或未找到交易对 {pair_symbol}. Msg: {error_msg}"}
  89. except requests.exceptions.RequestException as e:
  90. return {"error": f"Gate现货请求失败: {e}"}
  91. except Exception as e:
  92. return {"error": f"Gate现货意外错误: {e}"}
  93. def get_gateio_futures_price(contract_symbol, settle_currency='usdt'):
  94. # (代码与之前相同, 返回 {"price_settle_per_base_asset": decimal.Decimal(last_price_str)} 或 {"error": ...})
  95. url = f'https://api.gateio.ws/api/v4/futures/{settle_currency}/tickers'
  96. params = {'contract': contract_symbol}
  97. try:
  98. response = requests.get(url, params=params, proxies=proxies, timeout=10)
  99. response.raise_for_status()
  100. data = response.json()
  101. if isinstance(data, list) and len(data) > 0:
  102. ticker_data = data[0]
  103. if ticker_data.get('contract') == contract_symbol:
  104. last_price_str = ticker_data.get('last')
  105. if last_price_str:
  106. return {"price_settle_per_base_asset": decimal.Decimal(last_price_str)}
  107. else:
  108. return {"error": f"Gate期货 ({contract_symbol}) 未找到 'last' price"}
  109. else:
  110. return {"error": f"Gate期货 API 返回合约与请求 ({contract_symbol}) 不匹配"}
  111. else:
  112. error_msg = "未知错误或合约不存在"
  113. if isinstance(data, dict) and data.get('label'):
  114. error_msg = f"{data.get('label')}: {data.get('message', '')}"
  115. return {"error": f"Gate期货 API 数据格式错误或未找到合约 {contract_symbol}. Msg: {error_msg}"}
  116. except requests.exceptions.RequestException as e:
  117. return {"error": f"Gate期货请求失败: {e}"}
  118. except Exception as e:
  119. return {"error": f"Gate期货意外错误: {e}"}
  120. app = Flask(__name__)
  121. MAX_HISTORY_POINTS = 86400
  122. latest_data = {
  123. "oo_price": None,
  124. "gate_spot_price": None,
  125. "gate_futures_price": None,
  126. "diff_oo_vs_spot_percentage": None,
  127. "diff_oo_vs_futures_percentage": None,
  128. "diff_spot_vs_futures_percentage": None, # 基差
  129. "oo_error": None,
  130. "gate_spot_error": None,
  131. "gate_futures_error": None,
  132. "last_updated": None,
  133. "gate_spot_pair_name": GATEIO_SPOT_PAIR,
  134. "gate_futures_contract_name": GATEIO_FUTURES_CONTRACT
  135. }
  136. # 历史价格: oo, spot, futures
  137. historical_prices = deque(maxlen=MAX_HISTORY_POINTS)
  138. # 历史价差: oo_vs_spot, oo_vs_futures, spot_vs_futures
  139. historical_diffs = deque(maxlen=MAX_HISTORY_POINTS)
  140. data_lock = threading.Lock()
  141. def calculate_percentage_diff(price_a, price_b):
  142. """(price_a - price_b) / price_b * 100, B为基准"""
  143. if price_a is not None and price_b is not None and price_b > 0:
  144. diff = price_a - price_b
  145. return (diff / price_b) * 100
  146. return None
  147. def update_prices_periodically():
  148. global latest_data, historical_prices, historical_diffs
  149. while True:
  150. fetch_time = time.strftime("%H:%M:%S")
  151. oo_data = get_openocean_price_bsc(IN_TOKEN_ADDRESS_BSC, OUT_TOKEN_ADDRESS_BSC, AMOUNT_TO_QUERY_HUMAN)
  152. spot_data = get_gateio_spot_price(GATEIO_SPOT_PAIR)
  153. futures_data = get_gateio_futures_price(GATEIO_FUTURES_CONTRACT)
  154. oo_price = oo_data.get("price_usdt_per_out_token")
  155. spot_price = spot_data.get("price_base_in_quote")
  156. futures_price = futures_data.get("price_settle_per_base_asset")
  157. oo_err = oo_data.get("error")
  158. spot_err = spot_data.get("error")
  159. futures_err = futures_data.get("error")
  160. # 价差计算
  161. diff_oo_spot_pct = calculate_percentage_diff(oo_price, spot_price)
  162. diff_oo_futures_pct = calculate_percentage_diff(oo_price, futures_price)
  163. diff_spot_futures_pct = calculate_percentage_diff(spot_price, futures_price)
  164. # 存储历史价格
  165. historical_prices.append({
  166. "timestamp": fetch_time,
  167. "oo": float(oo_price) if oo_price else None,
  168. "spot": float(spot_price) if spot_price else None,
  169. "futures": float(futures_price) if futures_price else None,
  170. })
  171. # 存储历史价差
  172. historical_diffs.append({
  173. "timestamp": fetch_time,
  174. "oo_vs_spot": float(diff_oo_spot_pct) if diff_oo_spot_pct is not None else None,
  175. "oo_vs_futures": float(diff_oo_futures_pct) if diff_oo_futures_pct is not None else None,
  176. "spot_vs_futures": float(diff_spot_futures_pct) if diff_spot_futures_pct is not None else None,
  177. })
  178. with data_lock:
  179. latest_data["oo_price"] = str(oo_price) if oo_price else None
  180. latest_data["gate_spot_price"] = str(spot_price) if spot_price else None
  181. latest_data["gate_futures_price"] = str(futures_price) if futures_price else None
  182. latest_data[
  183. "diff_oo_vs_spot_percentage"] = f"{diff_oo_spot_pct:+.4f}%" if diff_oo_spot_pct is not None else "N/A"
  184. latest_data[
  185. "diff_oo_vs_futures_percentage"] = f"{diff_oo_futures_pct:+.4f}%" if diff_oo_futures_pct is not None else "N/A"
  186. latest_data[
  187. "diff_spot_vs_futures_percentage"] = f"{diff_spot_futures_pct:+.4f}%" if diff_spot_futures_pct is not None else "N/A"
  188. latest_data["oo_error"] = oo_err
  189. latest_data["gate_spot_error"] = spot_err
  190. latest_data["gate_futures_error"] = futures_err
  191. latest_data["last_updated"] = time.strftime("%Y-%m-%d %H:%M:%S")
  192. # gate_spot_pair_name & gate_futures_contract_name are set at init
  193. print(f"{fetch_time} | OO: {latest_data['oo_price']} (Err:{oo_err}) | "
  194. f"Spot({GATEIO_SPOT_PAIR}): {latest_data['gate_spot_price']} (Err:{spot_err}) | "
  195. f"Futures({GATEIO_FUTURES_CONTRACT}): {latest_data['gate_futures_price']} (Err:{futures_err}) | "
  196. f"D(OoS):{latest_data['diff_oo_vs_spot_percentage']} D(OoF):{latest_data['diff_oo_vs_futures_percentage']} D(SpF):{latest_data['diff_spot_vs_futures_percentage']}")
  197. time.sleep(1)
  198. @app.route('/')
  199. def index():
  200. app_config = {
  201. "GATEIO_SPOT_PAIR": GATEIO_SPOT_PAIR,
  202. "GATEIO_FUTURES_CONTRACT": GATEIO_FUTURES_CONTRACT,
  203. # 也可以把 OUT_TOKEN_ADDRESS_BSC 对应的代币符号传过去,如果能确定的话
  204. "TARGET_ASSET_SYMBOL": GATEIO_SPOT_PAIR.split('_')[0] # 假设命名规则为 ASSET_USDT
  205. }
  206. return render_template('index.html', config=app_config)
  207. @app.route('/data')
  208. def get_data():
  209. with data_lock:
  210. response_data = {
  211. "current": {**latest_data}, # Create a copy
  212. "history": {
  213. "prices": {
  214. "labels": [item['timestamp'] for item in historical_prices],
  215. "oo": [item['oo'] for item in historical_prices],
  216. "spot": [item['spot'] for item in historical_prices],
  217. "futures": [item['futures'] for item in historical_prices],
  218. },
  219. "diffs": { # Changed from "difference" to "diffs" to hold multiple series
  220. "labels": [item['timestamp'] for item in historical_diffs], # Should be same as price labels
  221. "oo_vs_spot": [item['oo_vs_spot'] for item in historical_diffs],
  222. "oo_vs_futures": [item['oo_vs_futures'] for item in historical_diffs],
  223. "spot_vs_futures": [item['spot_vs_futures'] for item in historical_diffs],
  224. }
  225. }
  226. }
  227. # Add config here too, so JS can access it if not from initial render_template
  228. response_data["current"]["config_gate_spot_pair"] = GATEIO_SPOT_PAIR
  229. response_data["current"]["config_gate_futures_contract"] = GATEIO_FUTURES_CONTRACT
  230. response_data["current"]["config_target_asset_symbol"] = GATEIO_SPOT_PAIR.split('_')[0]
  231. return jsonify(response_data)
  232. if __name__ == "__main__":
  233. werkzeug_logger = logging.getLogger('werkzeug')
  234. werkzeug_logger.setLevel(logging.ERROR)
  235. print(f"监控 OpenOcean ({OUT_TOKEN_ADDRESS_BSC.split('0x')[-1][:6]}...) vs USDT")
  236. print(f"Gate.io 现货: {GATEIO_SPOT_PAIR}")
  237. print(f"Gate.io 期货: {GATEIO_FUTURES_CONTRACT}")
  238. # Basic check for CAT example, adjust if your token is different
  239. asset_symbol = GATEIO_SPOT_PAIR.split('_')[0].upper()
  240. if asset_symbol == "CAT" and "0x6894CDe390a3f51155ea41Ed24a33A4827d3063D" not in OUT_TOKEN_ADDRESS_BSC:
  241. print(f"[警告] 配置的资产符号 'CAT' 可能与 OpenOcean 输出代币地址不完全对应。")
  242. elif asset_symbol not in GATEIO_FUTURES_CONTRACT.upper():
  243. print(f"[警告] 现货交易对 '{GATEIO_SPOT_PAIR}' 的基础资产与期货合约 '{GATEIO_FUTURES_CONTRACT}' 可能不匹配。")
  244. price_updater_thread = threading.Thread(target=update_prices_periodically, daemon=True)
  245. price_updater_thread.start()
  246. app.run(debug=True, host='0.0.0.0', port=5000, use_reloader=False)