|
@@ -3,281 +3,283 @@ import decimal
|
|
|
import time
|
|
import time
|
|
|
import threading
|
|
import threading
|
|
|
import json
|
|
import json
|
|
|
-from flask import Flask, render_template, jsonify
|
|
|
|
|
|
|
+from flask import Flask, render_template, jsonify # jsonify 仍然有用,但/data可能直接返回Plotly JSON字符串
|
|
|
from collections import deque
|
|
from collections import deque
|
|
|
import logging
|
|
import logging
|
|
|
|
|
|
|
|
-# --- 配置部分 ---
|
|
|
|
|
-# OpenOcean
|
|
|
|
|
-IN_TOKEN_ADDRESS_BSC = '0x55d398326f99059ff775485246999027b3197955' # USDT on BSC
|
|
|
|
|
-OUT_TOKEN_ADDRESS_BSC = '0x8F0528cE5eF7B51152A59745bEfDD91D97091d2F'
|
|
|
|
|
-AMOUNT_TO_QUERY_HUMAN = decimal.Decimal('1000') # 查询的USDT数量
|
|
|
|
|
|
|
+import plotly # 引入 Plotly
|
|
|
|
|
+import plotly.graph_objects as go
|
|
|
|
|
+from plotly.utils import PlotlyJSONEncoder # 用于将Plotly图表序列化为JSON
|
|
|
|
|
|
|
|
-# Gate.io 现货交易对
|
|
|
|
|
-GATEIO_SPOT_PAIR = 'ALPACA_USDT' # 示例, 请确保存在且与OUT_TOKEN_ADDRESS_BSC对应
|
|
|
|
|
-
|
|
|
|
|
-# Gate.io USDT 结算永续合约名称
|
|
|
|
|
-GATEIO_FUTURES_CONTRACT = 'ALPACA_USDT' # 示例, 请确保存在且与OUT_TOKEN_ADDRESS_BSC对应
|
|
|
|
|
-
|
|
|
|
|
-# 代理配置
|
|
|
|
|
|
|
+# --- 配置部分 (与之前相同) ---
|
|
|
|
|
+IN_TOKEN_ADDRESS_BSC = '0x55d398326f99059ff775485246999027b3197955'
|
|
|
|
|
+OUT_TOKEN_ADDRESS_BSC = '0x6894CDe390a3f51155ea41Ed24a33A4827d3063D' # CAT
|
|
|
|
|
+AMOUNT_TO_QUERY_HUMAN = decimal.Decimal('1000')
|
|
|
|
|
+GATEIO_SPOT_PAIR = 'CAT_USDT'
|
|
|
|
|
+GATEIO_FUTURES_CONTRACT = 'CAT_USDT'
|
|
|
proxies = None
|
|
proxies = None
|
|
|
-# PROXY_HOST = '127.0.0.1'
|
|
|
|
|
-# PROXY_PORT = '7890'
|
|
|
|
|
-# proxies = {
|
|
|
|
|
-# 'http': f'http://{PROXY_HOST}:{PROXY_PORT}',
|
|
|
|
|
-# 'https': f'http://{PROXY_HOST}:{PROXY_PORT}',
|
|
|
|
|
-# }
|
|
|
|
|
-
|
|
|
|
|
decimal.getcontext().prec = 36
|
|
decimal.getcontext().prec = 36
|
|
|
|
|
|
|
|
|
|
|
|
|
-# --- 价格获取函数 ---
|
|
|
|
|
|
|
+# --- 价格获取函数 (与之前相同, 省略以保持简洁) ---
|
|
|
def get_openocean_price_bsc(in_token_addr, out_token_addr, human_amount_in_decimal_for_request, gas_price=3):
|
|
def get_openocean_price_bsc(in_token_addr, out_token_addr, human_amount_in_decimal_for_request, gas_price=3):
|
|
|
- # (代码与之前相同, 返回 {"price_usdt_per_out_token": calculated_price} 或 {"error": ...})
|
|
|
|
|
- chain = 'bsc'
|
|
|
|
|
|
|
+ chain = 'bsc';
|
|
|
url = f'https://open-api.openocean.finance/v4/{chain}/quote'
|
|
url = f'https://open-api.openocean.finance/v4/{chain}/quote'
|
|
|
- params = {
|
|
|
|
|
- 'inTokenAddress': in_token_addr,
|
|
|
|
|
- 'outTokenAddress': out_token_addr,
|
|
|
|
|
- 'amount': str(human_amount_in_decimal_for_request),
|
|
|
|
|
- 'gasPrice': gas_price,
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ params = {'inTokenAddress': in_token_addr, 'outTokenAddress': out_token_addr,
|
|
|
|
|
+ 'amount': str(human_amount_in_decimal_for_request), 'gasPrice': gas_price}
|
|
|
try:
|
|
try:
|
|
|
- response = requests.get(url, params=params, proxies=proxies, timeout=10)
|
|
|
|
|
- response.raise_for_status()
|
|
|
|
|
- data = response.json()
|
|
|
|
|
|
|
+ r = requests.get(url, params=params, proxies=proxies, timeout=10);
|
|
|
|
|
+ r.raise_for_status();
|
|
|
|
|
+ data = r.json()
|
|
|
if data.get('code') == 200 and data.get('data'):
|
|
if data.get('code') == 200 and data.get('data'):
|
|
|
- api_data = data['data']
|
|
|
|
|
- required_keys = ['inToken', 'outToken', 'inAmount', 'outAmount']
|
|
|
|
|
- if not all(key in api_data for key in required_keys) or \
|
|
|
|
|
- api_data['inToken'].get('decimals') is None or \
|
|
|
|
|
- api_data['outToken'].get('decimals') is None:
|
|
|
|
|
- return {"error": "OO API响应中缺少必要的数据"}
|
|
|
|
|
-
|
|
|
|
|
- in_token_decimals = int(api_data['inToken']['decimals'])
|
|
|
|
|
- out_token_decimals = int(api_data['outToken']['decimals'])
|
|
|
|
|
- atomic_in_amount = decimal.Decimal(api_data['inAmount'])
|
|
|
|
|
- atomic_out_amount = decimal.Decimal(api_data['outAmount'])
|
|
|
|
|
- human_in_amount_used = atomic_in_amount / (decimal.Decimal('10') ** in_token_decimals)
|
|
|
|
|
- human_out_amount_received = atomic_out_amount / (decimal.Decimal('10') ** out_token_decimals)
|
|
|
|
|
- if human_out_amount_received == 0: return {"error": "OO 计算的输出数量为零"}
|
|
|
|
|
- calculated_price = human_in_amount_used / human_out_amount_received
|
|
|
|
|
- return {"price_usdt_per_out_token": calculated_price}
|
|
|
|
|
|
|
+ d = data['data'];
|
|
|
|
|
+ req = ['inToken', 'outToken', 'inAmount', 'outAmount']
|
|
|
|
|
+ if not all(k in d for k in req) or d['inToken'].get('decimals') is None or d['outToken'].get(
|
|
|
|
|
+ 'decimals') is None: return {"error": "OO API缺少数据"}
|
|
|
|
|
+ in_dec, out_dec = int(d['inToken']['decimals']), int(d['outToken']['decimals'])
|
|
|
|
|
+ atomic_in, atomic_out = decimal.Decimal(d['inAmount']), decimal.Decimal(d['outAmount'])
|
|
|
|
|
+ h_in, h_out = atomic_in / (10 ** in_dec), atomic_out / (10 ** out_dec)
|
|
|
|
|
+ if h_out == 0: return {"error": "OO输出为0"}
|
|
|
|
|
+ return {"price_usdt_per_out_token": h_in / h_out}
|
|
|
else:
|
|
else:
|
|
|
- error_message = data.get('message', 'N/A') if data else '无响应数据'
|
|
|
|
|
- error_code = data.get('code', 'N/A') if data else 'N/A'
|
|
|
|
|
- return {"error": f"OO API 错误 - Code: {error_code}, Msg: {error_message}"}
|
|
|
|
|
- except requests.exceptions.RequestException as e:
|
|
|
|
|
- return {"error": f"OO请求失败: {e}"}
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ "error": f"OO API错误 - Code:{data.get('code', 'N/A')}, Msg:{data.get('msg', data.get('message', 'N/A')) if isinstance(data, dict) else '格式错误'}"}
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- return {"error": f"OO意外错误: {e}"}
|
|
|
|
|
|
|
+ return {"error": f"OO请求错误: {e}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_gateio_spot_price(pair_symbol):
|
|
def get_gateio_spot_price(pair_symbol):
|
|
|
- # (代码与之前相同, 返回 {"price_base_in_quote": decimal.Decimal(last_price_str)} 或 {"error": ...})
|
|
|
|
|
- url = f'https://api.gateio.ws/api/v4/spot/tickers'
|
|
|
|
|
|
|
+ url = f'https://api.gateio.ws/api/v4/spot/tickers';
|
|
|
params = {'currency_pair': pair_symbol}
|
|
params = {'currency_pair': pair_symbol}
|
|
|
try:
|
|
try:
|
|
|
- response = requests.get(url, params=params, proxies=proxies, timeout=10)
|
|
|
|
|
- response.raise_for_status()
|
|
|
|
|
- data = response.json()
|
|
|
|
|
- if isinstance(data, list) and len(data) > 0:
|
|
|
|
|
- ticker_data = data[0]
|
|
|
|
|
- if ticker_data.get('currency_pair') == pair_symbol:
|
|
|
|
|
- last_price_str = ticker_data.get('last')
|
|
|
|
|
- if last_price_str:
|
|
|
|
|
- return {"price_base_in_quote": decimal.Decimal(last_price_str)}
|
|
|
|
|
- else:
|
|
|
|
|
- return {"error": "Gate现货未找到last price"}
|
|
|
|
|
|
|
+ r = requests.get(url, params=params, proxies=proxies, timeout=10);
|
|
|
|
|
+ r.raise_for_status();
|
|
|
|
|
+ data = r.json()
|
|
|
|
|
+ if isinstance(data, list) and data:
|
|
|
|
|
+ td = data[0]
|
|
|
|
|
+ if td.get('currency_pair') == pair_symbol and td.get('last'):
|
|
|
|
|
+ return {"price_base_in_quote": decimal.Decimal(td['last'])}
|
|
|
else:
|
|
else:
|
|
|
- return {"error": f"Gate现货交易对不匹配"}
|
|
|
|
|
|
|
+ return {"error": f"Gate现货数据不匹配(Req:{pair_symbol},Res:{td.get('currency_pair')})或无价格"}
|
|
|
|
|
+ elif isinstance(data, dict) and data.get('label'):
|
|
|
|
|
+ return {"error": f"Gate现货API错误: {data.get('label')}-{data.get('message', '')}"}
|
|
|
else:
|
|
else:
|
|
|
- error_msg = "未知错误或交易对不存在"
|
|
|
|
|
- if isinstance(data, dict) and data.get('label'):
|
|
|
|
|
- error_msg = f"{data.get('label')}: {data.get('message', '')}"
|
|
|
|
|
- return {"error": f"Gate现货 API 数据格式错误或未找到交易对 {pair_symbol}. Msg: {error_msg}"}
|
|
|
|
|
- except requests.exceptions.RequestException as e:
|
|
|
|
|
- return {"error": f"Gate现货请求失败: {e}"}
|
|
|
|
|
|
|
+ return {"error": f"Gate现货API数据格式错误或未找到 {pair_symbol}"}
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- return {"error": f"Gate现货意外错误: {e}"}
|
|
|
|
|
|
|
+ return {"error": f"Gate现货请求错误: {e}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_gateio_futures_price(contract_symbol, settle_currency='usdt'):
|
|
def get_gateio_futures_price(contract_symbol, settle_currency='usdt'):
|
|
|
- # (代码与之前相同, 返回 {"price_settle_per_base_asset": decimal.Decimal(last_price_str)} 或 {"error": ...})
|
|
|
|
|
- url = f'https://api.gateio.ws/api/v4/futures/{settle_currency}/tickers'
|
|
|
|
|
|
|
+ url = f'https://api.gateio.ws/api/v4/futures/{settle_currency}/tickers';
|
|
|
params = {'contract': contract_symbol}
|
|
params = {'contract': contract_symbol}
|
|
|
try:
|
|
try:
|
|
|
- response = requests.get(url, params=params, proxies=proxies, timeout=10)
|
|
|
|
|
- response.raise_for_status()
|
|
|
|
|
- data = response.json()
|
|
|
|
|
- if isinstance(data, list) and len(data) > 0:
|
|
|
|
|
- ticker_data = data[0]
|
|
|
|
|
- if ticker_data.get('contract') == contract_symbol:
|
|
|
|
|
- last_price_str = ticker_data.get('last')
|
|
|
|
|
- if last_price_str:
|
|
|
|
|
- return {"price_settle_per_base_asset": decimal.Decimal(last_price_str)}
|
|
|
|
|
- else:
|
|
|
|
|
- return {"error": f"Gate期货 ({contract_symbol}) 未找到 'last' price"}
|
|
|
|
|
|
|
+ r = requests.get(url, params=params, proxies=proxies, timeout=10);
|
|
|
|
|
+ r.raise_for_status();
|
|
|
|
|
+ data = r.json()
|
|
|
|
|
+ if isinstance(data, list) and data:
|
|
|
|
|
+ td = data[0]
|
|
|
|
|
+ if td.get('contract') == contract_symbol and td.get('last'):
|
|
|
|
|
+ return {"price_settle_per_base_asset": decimal.Decimal(td['last'])}
|
|
|
else:
|
|
else:
|
|
|
- return {"error": f"Gate期货 API 返回合约与请求 ({contract_symbol}) 不匹配"}
|
|
|
|
|
|
|
+ return {"error": f"Gate期货数据不匹配(Req:{contract_symbol},Res:{td.get('contract')})或无价格"}
|
|
|
|
|
+ elif isinstance(data, dict) and data.get('label'):
|
|
|
|
|
+ return {"error": f"Gate期货API错误: {data.get('label')}-{data.get('message', '')}"}
|
|
|
else:
|
|
else:
|
|
|
- error_msg = "未知错误或合约不存在"
|
|
|
|
|
- if isinstance(data, dict) and data.get('label'):
|
|
|
|
|
- error_msg = f"{data.get('label')}: {data.get('message', '')}"
|
|
|
|
|
- return {"error": f"Gate期货 API 数据格式错误或未找到合约 {contract_symbol}. Msg: {error_msg}"}
|
|
|
|
|
- except requests.exceptions.RequestException as e:
|
|
|
|
|
- return {"error": f"Gate期货请求失败: {e}"}
|
|
|
|
|
|
|
+ return {"error": f"Gate期货API数据格式错误或未找到 {contract_symbol}"}
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- return {"error": f"Gate期货意外错误: {e}"}
|
|
|
|
|
|
|
+ return {"error": f"Gate期货请求错误: {e}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
app = Flask(__name__)
|
|
|
-
|
|
|
|
|
-MAX_HISTORY_POINTS = 86400
|
|
|
|
|
-latest_data = {
|
|
|
|
|
- "oo_price": None,
|
|
|
|
|
- "gate_spot_price": None,
|
|
|
|
|
- "gate_futures_price": None,
|
|
|
|
|
- "diff_oo_vs_spot_percentage": None,
|
|
|
|
|
- "diff_oo_vs_futures_percentage": None,
|
|
|
|
|
- "diff_spot_vs_futures_percentage": None, # 基差
|
|
|
|
|
- "oo_error": None,
|
|
|
|
|
- "gate_spot_error": None,
|
|
|
|
|
- "gate_futures_error": None,
|
|
|
|
|
- "last_updated": None,
|
|
|
|
|
|
|
+log = logging.getLogger('werkzeug');
|
|
|
|
|
+log.setLevel(logging.ERROR)
|
|
|
|
|
+
|
|
|
|
|
+MAX_HISTORY_POINTS_PLOTLY = 300 # Plotly 图表显示的点数
|
|
|
|
|
+REFRESH_INTERVAL_SECONDS = 1 # 后端数据更新频率
|
|
|
|
|
+
|
|
|
|
|
+# 存储原始数据点,用于 Plotly 生成图表
|
|
|
|
|
+historical_data_points = deque(maxlen=MAX_HISTORY_POINTS_PLOTLY)
|
|
|
|
|
+# 存储最新的数值,用于表格展示 (这个结构可以保留)
|
|
|
|
|
+latest_values_for_table = {
|
|
|
|
|
+ "oo_price": "N/A", "gate_spot_price": "N/A", "gate_futures_price": "N/A",
|
|
|
|
|
+ "diff_oo_vs_spot_percentage": "N/A", "diff_oo_vs_futures_percentage": "N/A",
|
|
|
|
|
+ "diff_spot_vs_futures_percentage": "N/A",
|
|
|
|
|
+ "oo_error": None, "gate_spot_error": None, "gate_futures_error": None,
|
|
|
|
|
+ "last_updated": "N/A",
|
|
|
"gate_spot_pair_name": GATEIO_SPOT_PAIR,
|
|
"gate_spot_pair_name": GATEIO_SPOT_PAIR,
|
|
|
- "gate_futures_contract_name": GATEIO_FUTURES_CONTRACT
|
|
|
|
|
|
|
+ "gate_futures_contract_name": GATEIO_FUTURES_CONTRACT,
|
|
|
|
|
+ "target_asset_symbol": GATEIO_SPOT_PAIR.split('_')[0]
|
|
|
}
|
|
}
|
|
|
-# 历史价格: oo, spot, futures
|
|
|
|
|
-historical_prices = deque(maxlen=MAX_HISTORY_POINTS)
|
|
|
|
|
-# 历史价差: oo_vs_spot, oo_vs_futures, spot_vs_futures
|
|
|
|
|
-historical_diffs = deque(maxlen=MAX_HISTORY_POINTS)
|
|
|
|
|
-
|
|
|
|
|
-data_lock = threading.Lock()
|
|
|
|
|
|
|
+data_lock = threading.Lock() # 保护共享数据
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_percentage_diff(price_a, price_b):
|
|
def calculate_percentage_diff(price_a, price_b):
|
|
|
- """(price_a - price_b) / price_b * 100, B为基准"""
|
|
|
|
|
- if price_a is not None and price_b is not None and price_b > 0:
|
|
|
|
|
- diff = price_a - price_b
|
|
|
|
|
- return (diff / price_b) * 100
|
|
|
|
|
|
|
+ if price_a is not None and price_b is not None and isinstance(price_a, decimal.Decimal) and isinstance(price_b,
|
|
|
|
|
+ decimal.Decimal) and price_b > 0:
|
|
|
|
|
+ return ((price_a - price_b) / price_b) * 100
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
-def update_prices_periodically():
|
|
|
|
|
- global latest_data, historical_prices, historical_diffs
|
|
|
|
|
|
|
+def update_data_for_plotly_and_table():
|
|
|
|
|
+ global historical_data_points, latest_values_for_table
|
|
|
|
|
+ print("数据更新线程启动...")
|
|
|
while True:
|
|
while True:
|
|
|
- fetch_time = time.strftime("%H:%M:%S")
|
|
|
|
|
|
|
+ fetch_time_full = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
+ fetch_time_chart = time.strftime("%H:%M:%S") # 或者使用 time.time() 获取更精确的unix时间戳
|
|
|
|
|
|
|
|
oo_data = get_openocean_price_bsc(IN_TOKEN_ADDRESS_BSC, OUT_TOKEN_ADDRESS_BSC, AMOUNT_TO_QUERY_HUMAN)
|
|
oo_data = get_openocean_price_bsc(IN_TOKEN_ADDRESS_BSC, OUT_TOKEN_ADDRESS_BSC, AMOUNT_TO_QUERY_HUMAN)
|
|
|
spot_data = get_gateio_spot_price(GATEIO_SPOT_PAIR)
|
|
spot_data = get_gateio_spot_price(GATEIO_SPOT_PAIR)
|
|
|
futures_data = get_gateio_futures_price(GATEIO_FUTURES_CONTRACT)
|
|
futures_data = get_gateio_futures_price(GATEIO_FUTURES_CONTRACT)
|
|
|
|
|
|
|
|
- oo_price = oo_data.get("price_usdt_per_out_token")
|
|
|
|
|
- spot_price = spot_data.get("price_base_in_quote")
|
|
|
|
|
- futures_price = futures_data.get("price_settle_per_base_asset")
|
|
|
|
|
|
|
+ oo_price = oo_data.get("price_usdt_per_out_token") # decimal or None
|
|
|
|
|
+ spot_price = spot_data.get("price_base_in_quote") # decimal or None
|
|
|
|
|
+ futures_price = futures_data.get("price_settle_per_base_asset") # decimal or None
|
|
|
|
|
|
|
|
oo_err = oo_data.get("error")
|
|
oo_err = oo_data.get("error")
|
|
|
spot_err = spot_data.get("error")
|
|
spot_err = spot_data.get("error")
|
|
|
futures_err = futures_data.get("error")
|
|
futures_err = futures_data.get("error")
|
|
|
|
|
|
|
|
- # 价差计算
|
|
|
|
|
diff_oo_spot_pct = calculate_percentage_diff(oo_price, spot_price)
|
|
diff_oo_spot_pct = calculate_percentage_diff(oo_price, spot_price)
|
|
|
diff_oo_futures_pct = calculate_percentage_diff(oo_price, futures_price)
|
|
diff_oo_futures_pct = calculate_percentage_diff(oo_price, futures_price)
|
|
|
diff_spot_futures_pct = calculate_percentage_diff(spot_price, futures_price)
|
|
diff_spot_futures_pct = calculate_percentage_diff(spot_price, futures_price)
|
|
|
|
|
|
|
|
- # 存储历史价格
|
|
|
|
|
- historical_prices.append({
|
|
|
|
|
- "timestamp": fetch_time,
|
|
|
|
|
- "oo": float(oo_price) if oo_price else None,
|
|
|
|
|
- "spot": float(spot_price) if spot_price else None,
|
|
|
|
|
- "futures": float(futures_price) if futures_price else None,
|
|
|
|
|
- })
|
|
|
|
|
- # 存储历史价差
|
|
|
|
|
- historical_diffs.append({
|
|
|
|
|
- "timestamp": fetch_time,
|
|
|
|
|
- "oo_vs_spot": float(diff_oo_spot_pct) if diff_oo_spot_pct is not None else None,
|
|
|
|
|
- "oo_vs_futures": float(diff_oo_futures_pct) if diff_oo_futures_pct is not None else None,
|
|
|
|
|
- "spot_vs_futures": float(diff_spot_futures_pct) if diff_spot_futures_pct is not None else None,
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ current_point = {
|
|
|
|
|
+ "time": fetch_time_chart, # 使用时间字符串作为X轴,Plotly可以处理
|
|
|
|
|
+ "oo_price": float(oo_price) if oo_price else None,
|
|
|
|
|
+ "spot_price": float(spot_price) if spot_price else None,
|
|
|
|
|
+ "futures_price": float(futures_price) if futures_price else None,
|
|
|
|
|
+ "diff_oo_spot": float(diff_oo_spot_pct) if diff_oo_spot_pct is not None else None,
|
|
|
|
|
+ "diff_oo_futures": float(diff_oo_futures_pct) if diff_oo_futures_pct is not None else None,
|
|
|
|
|
+ "diff_spot_futures": float(diff_spot_futures_pct) if diff_spot_futures_pct is not None else None,
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
with data_lock:
|
|
with data_lock:
|
|
|
- latest_data["oo_price"] = str(oo_price) if oo_price else None
|
|
|
|
|
- latest_data["gate_spot_price"] = str(spot_price) if spot_price else None
|
|
|
|
|
- latest_data["gate_futures_price"] = str(futures_price) if futures_price else None
|
|
|
|
|
- latest_data[
|
|
|
|
|
|
|
+ historical_data_points.append(current_point)
|
|
|
|
|
+
|
|
|
|
|
+ latest_values_for_table["oo_price"] = str(oo_price) if oo_price else "N/A"
|
|
|
|
|
+ latest_values_for_table["gate_spot_price"] = str(spot_price) if spot_price else "N/A"
|
|
|
|
|
+ latest_values_for_table["gate_futures_price"] = str(futures_price) if futures_price else "N/A"
|
|
|
|
|
+ latest_values_for_table[
|
|
|
"diff_oo_vs_spot_percentage"] = f"{diff_oo_spot_pct:+.4f}%" if diff_oo_spot_pct is not None else "N/A"
|
|
"diff_oo_vs_spot_percentage"] = f"{diff_oo_spot_pct:+.4f}%" if diff_oo_spot_pct is not None else "N/A"
|
|
|
- latest_data[
|
|
|
|
|
|
|
+ latest_values_for_table[
|
|
|
"diff_oo_vs_futures_percentage"] = f"{diff_oo_futures_pct:+.4f}%" if diff_oo_futures_pct is not None else "N/A"
|
|
"diff_oo_vs_futures_percentage"] = f"{diff_oo_futures_pct:+.4f}%" if diff_oo_futures_pct is not None else "N/A"
|
|
|
- latest_data[
|
|
|
|
|
|
|
+ latest_values_for_table[
|
|
|
"diff_spot_vs_futures_percentage"] = f"{diff_spot_futures_pct:+.4f}%" if diff_spot_futures_pct is not None else "N/A"
|
|
"diff_spot_vs_futures_percentage"] = f"{diff_spot_futures_pct:+.4f}%" if diff_spot_futures_pct is not None else "N/A"
|
|
|
- latest_data["oo_error"] = oo_err
|
|
|
|
|
- latest_data["gate_spot_error"] = spot_err
|
|
|
|
|
- latest_data["gate_futures_error"] = futures_err
|
|
|
|
|
- latest_data["last_updated"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
- # gate_spot_pair_name & gate_futures_contract_name are set at init
|
|
|
|
|
|
|
+ latest_values_for_table["oo_error"] = oo_err
|
|
|
|
|
+ latest_values_for_table["gate_spot_error"] = spot_err
|
|
|
|
|
+ latest_values_for_table["gate_futures_error"] = futures_err
|
|
|
|
|
+ latest_values_for_table["last_updated"] = fetch_time_full
|
|
|
|
|
|
|
|
- print(f"{fetch_time} | OO: {latest_data['oo_price']} (Err:{oo_err}) | "
|
|
|
|
|
- f"Spot({GATEIO_SPOT_PAIR}): {latest_data['gate_spot_price']} (Err:{spot_err}) | "
|
|
|
|
|
- f"Futures({GATEIO_FUTURES_CONTRACT}): {latest_data['gate_futures_price']} (Err:{futures_err}) | "
|
|
|
|
|
- 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']}")
|
|
|
|
|
|
|
+ # Log a summary, not too verbose
|
|
|
|
|
+ print(f"{fetch_time_chart} Update | "
|
|
|
|
|
+ f"OO:{'OK' if oo_price else 'Fail'} "
|
|
|
|
|
+ f"GS:{'OK' if spot_price else 'Fail'} "
|
|
|
|
|
+ f"GF:{'OK' if futures_price else 'Fail'}")
|
|
|
|
|
|
|
|
- time.sleep(1)
|
|
|
|
|
|
|
+ time.sleep(REFRESH_INTERVAL_SECONDS)
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/')
|
|
@app.route('/')
|
|
|
-def index():
|
|
|
|
|
- app_config = {
|
|
|
|
|
- "GATEIO_SPOT_PAIR": GATEIO_SPOT_PAIR,
|
|
|
|
|
- "GATEIO_FUTURES_CONTRACT": GATEIO_FUTURES_CONTRACT,
|
|
|
|
|
- # 也可以把 OUT_TOKEN_ADDRESS_BSC 对应的代币符号传过去,如果能确定的话
|
|
|
|
|
- "TARGET_ASSET_SYMBOL": GATEIO_SPOT_PAIR.split('_')[0] # 假设命名规则为 ASSET_USDT
|
|
|
|
|
- }
|
|
|
|
|
- return render_template('index.html', config=app_config)
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-@app.route('/data')
|
|
|
|
|
-def get_data():
|
|
|
|
|
|
|
+def index_plotly():
|
|
|
|
|
+ # 将配置传递给模板,用于标题等
|
|
|
|
|
+ target_asset = GATEIO_SPOT_PAIR.split('_')[0]
|
|
|
|
|
+ return render_template('index_plotly.html',
|
|
|
|
|
+ target_asset=target_asset,
|
|
|
|
|
+ spot_pair=GATEIO_SPOT_PAIR,
|
|
|
|
|
+ futures_contract=GATEIO_FUTURES_CONTRACT,
|
|
|
|
|
+ refresh_interval_ms=REFRESH_INTERVAL_SECONDS * 1000)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.route('/table-data') # 新增一个专门用于表格数据的API
|
|
|
|
|
+def get_table_data():
|
|
|
with data_lock:
|
|
with data_lock:
|
|
|
- response_data = {
|
|
|
|
|
- "current": {**latest_data}, # Create a copy
|
|
|
|
|
- "history": {
|
|
|
|
|
- "prices": {
|
|
|
|
|
- "labels": [item['timestamp'] for item in historical_prices],
|
|
|
|
|
- "oo": [item['oo'] for item in historical_prices],
|
|
|
|
|
- "spot": [item['spot'] for item in historical_prices],
|
|
|
|
|
- "futures": [item['futures'] for item in historical_prices],
|
|
|
|
|
- },
|
|
|
|
|
- "diffs": { # Changed from "difference" to "diffs" to hold multiple series
|
|
|
|
|
- "labels": [item['timestamp'] for item in historical_diffs], # Should be same as price labels
|
|
|
|
|
- "oo_vs_spot": [item['oo_vs_spot'] for item in historical_diffs],
|
|
|
|
|
- "oo_vs_futures": [item['oo_vs_futures'] for item in historical_diffs],
|
|
|
|
|
- "spot_vs_futures": [item['spot_vs_futures'] for item in historical_diffs],
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- # Add config here too, so JS can access it if not from initial render_template
|
|
|
|
|
- response_data["current"]["config_gate_spot_pair"] = GATEIO_SPOT_PAIR
|
|
|
|
|
- response_data["current"]["config_gate_futures_contract"] = GATEIO_FUTURES_CONTRACT
|
|
|
|
|
- response_data["current"]["config_target_asset_symbol"] = GATEIO_SPOT_PAIR.split('_')[0]
|
|
|
|
|
|
|
+ return jsonify(latest_values_for_table)
|
|
|
|
|
+
|
|
|
|
|
|
|
|
- return jsonify(response_data)
|
|
|
|
|
|
|
+@app.route('/plotly-chart-data') # 用于Plotly图表的JSON数据
|
|
|
|
|
+def get_plotly_chart_data():
|
|
|
|
|
+ with data_lock:
|
|
|
|
|
+ # 从 deque 转换为列表,方便 Plotly 处理
|
|
|
|
|
+ points = list(historical_data_points)
|
|
|
|
|
+ if not points: # 如果没有数据点,返回空图表结构或错误提示
|
|
|
|
|
+ fig = go.Figure()
|
|
|
|
|
+ fig.update_layout(title_text="暂无数据", xaxis_title="时间", yaxis_title="价格/百分比")
|
|
|
|
|
+ return json.dumps(fig, cls=PlotlyJSONEncoder)
|
|
|
|
|
+
|
|
|
|
|
+ times = [p['time'] for p in points]
|
|
|
|
|
+
|
|
|
|
|
+ # --- 创建价格图 (主图) ---
|
|
|
|
|
+ fig_prices = go.Figure()
|
|
|
|
|
+ fig_prices.add_trace(go.Scatter(x=times, y=[p['oo_price'] for p in points], mode='lines', name='OpenOcean',
|
|
|
|
|
+ line=dict(color='rgb(75, 192, 192)')))
|
|
|
|
|
+ fig_prices.add_trace(go.Scatter(x=times, y=[p['spot_price'] for p in points], mode='lines',
|
|
|
|
|
+ name=f'Gate Spot ({GATEIO_SPOT_PAIR})', line=dict(color='rgb(255, 99, 132)')))
|
|
|
|
|
+ fig_prices.add_trace(go.Scatter(x=times, y=[p['futures_price'] for p in points], mode='lines',
|
|
|
|
|
+ name=f'Gate Futures ({GATEIO_FUTURES_CONTRACT})',
|
|
|
|
|
+ line=dict(color='rgb(54, 162, 235)')))
|
|
|
|
|
+
|
|
|
|
|
+ target_asset = GATEIO_SPOT_PAIR.split('_')[0]
|
|
|
|
|
+ fig_prices.update_layout(
|
|
|
|
|
+ title_text=f'{target_asset}/USDT 价格历史',
|
|
|
|
|
+ xaxis_title='时间',
|
|
|
|
|
+ yaxis_title=f'价格 ({target_asset}/USDT)',
|
|
|
|
|
+ legend_title_text='平台',
|
|
|
|
|
+ # height=400 # 可以为每个子图设置高度
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # --- 创建价差图 (副图) ---
|
|
|
|
|
+ fig_diffs = go.Figure()
|
|
|
|
|
+ fig_diffs.add_trace(go.Scatter(x=times, y=[p['diff_oo_spot'] for p in points], mode='lines',
|
|
|
|
|
+ name=f'OO vs Spot ({GATEIO_SPOT_PAIR})', line=dict(color='rgb(255, 159, 64)')))
|
|
|
|
|
+ fig_diffs.add_trace(go.Scatter(x=times, y=[p['diff_oo_futures'] for p in points], mode='lines',
|
|
|
|
|
+ name=f'OO vs Futures ({GATEIO_FUTURES_CONTRACT})',
|
|
|
|
|
+ line=dict(color='rgb(153, 102, 255)')))
|
|
|
|
|
+ fig_diffs.add_trace(go.Scatter(x=times, y=[p['diff_spot_futures'] for p in points], mode='lines',
|
|
|
|
|
+ name=f'Spot ({GATEIO_SPOT_PAIR}) vs Futures ({GATEIO_FUTURES_CONTRACT})',
|
|
|
|
|
+ line=dict(color='rgb(75, 192, 75)')))
|
|
|
|
|
+
|
|
|
|
|
+ fig_diffs.update_layout(
|
|
|
|
|
+ title_text='价差百分比历史',
|
|
|
|
|
+ xaxis_title='时间',
|
|
|
|
|
+ yaxis_title='价差 (%)',
|
|
|
|
|
+ legend_title_text='对比',
|
|
|
|
|
+ yaxis_zeroline=True, # 在y=0处画一条线
|
|
|
|
|
+ # height=300
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Plotly 不直接支持通过 fig.to_json() 合并具有完全独立Y轴的子图到一个figure中并保持良好交互性
|
|
|
|
|
+ # 最简单的方式是前端请求两个独立的图表JSON,然后分别渲染。
|
|
|
|
|
+ # 或者,我们可以使用 make_subplots,但共享X轴是关键。
|
|
|
|
|
+
|
|
|
|
|
+ # 为了简化前端,我们这里返回两个独立的Figure的JSON。前端需要能处理这个结构。
|
|
|
|
|
+ # 或者,更常见的是,前端请求两个不同的API端点来获取图表数据。
|
|
|
|
|
+ # 这里,我们将返回一个包含两个图表定义的字典。
|
|
|
|
|
+
|
|
|
|
|
+ combined_figure_data = {
|
|
|
|
|
+ "price_chart": json.loads(json.dumps(fig_prices, cls=PlotlyJSONEncoder)), # 确保可JSON序列化
|
|
|
|
|
+ "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder))
|
|
|
|
|
+ }
|
|
|
|
|
+ return jsonify(combined_figure_data) # jsonify 会自动处理JSON序列化
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if __name__ == "__main__":
|
|
|
- werkzeug_logger = logging.getLogger('werkzeug')
|
|
|
|
|
- werkzeug_logger.setLevel(logging.ERROR)
|
|
|
|
|
-
|
|
|
|
|
- print(f"监控 OpenOcean ({OUT_TOKEN_ADDRESS_BSC.split('0x')[-1][:6]}...) vs USDT")
|
|
|
|
|
|
|
+ print("应用启动...")
|
|
|
|
|
+ print(f"监控 OpenOcean (BSC代币: ...{OUT_TOKEN_ADDRESS_BSC[-6:]}) vs USDT")
|
|
|
print(f"Gate.io 现货: {GATEIO_SPOT_PAIR}")
|
|
print(f"Gate.io 现货: {GATEIO_SPOT_PAIR}")
|
|
|
print(f"Gate.io 期货: {GATEIO_FUTURES_CONTRACT}")
|
|
print(f"Gate.io 期货: {GATEIO_FUTURES_CONTRACT}")
|
|
|
- # Basic check for CAT example, adjust if your token is different
|
|
|
|
|
- asset_symbol = GATEIO_SPOT_PAIR.split('_')[0].upper()
|
|
|
|
|
- if asset_symbol == "CAT" and "0x6894CDe390a3f51155ea41Ed24a33A4827d3063D" not in OUT_TOKEN_ADDRESS_BSC:
|
|
|
|
|
- print(f"[警告] 配置的资产符号 'CAT' 可能与 OpenOcean 输出代币地址不完全对应。")
|
|
|
|
|
- elif asset_symbol not in GATEIO_FUTURES_CONTRACT.upper():
|
|
|
|
|
- print(f"[警告] 现货交易对 '{GATEIO_SPOT_PAIR}' 的基础资产与期货合约 '{GATEIO_FUTURES_CONTRACT}' 可能不匹配。")
|
|
|
|
|
-
|
|
|
|
|
- price_updater_thread = threading.Thread(target=update_prices_periodically, daemon=True)
|
|
|
|
|
- price_updater_thread.start()
|
|
|
|
|
- app.run(debug=True, host='0.0.0.0', port=5000, use_reloader=False)
|
|
|
|
|
|
|
+ # ... (启动时的配置检查可以保留) ...
|
|
|
|
|
+ spot_base = GATEIO_SPOT_PAIR.split('_')[0].upper()
|
|
|
|
|
+ futures_base = GATEIO_FUTURES_CONTRACT.split('_')[0].upper()
|
|
|
|
|
+ oo_asset_placeholder = "CAT" if "0x6894CDe390a3f51155ea41Ed24a33A4827d3063D" in OUT_TOKEN_ADDRESS_BSC else "TOKEN"
|
|
|
|
|
+
|
|
|
|
|
+ if oo_asset_placeholder != spot_base: print(
|
|
|
|
|
+ f"[警告] OO资产('{oo_asset_placeholder}')与Gate现货基础资产('{spot_base}')可能不匹配!")
|
|
|
|
|
+ if oo_asset_placeholder != futures_base: print(
|
|
|
|
|
+ f"[警告] OO资产('{oo_asset_placeholder}')与Gate期货基础资产('{futures_base}')可能不匹配!")
|
|
|
|
|
+ if spot_base != futures_base: print(
|
|
|
|
|
+ f"[警告] Gate现货基础资产('{spot_base}')与期货基础资产('{futures_base}')不匹配!")
|
|
|
|
|
+
|
|
|
|
|
+ data_thread = threading.Thread(target=update_data_for_plotly_and_table, daemon=True)
|
|
|
|
|
+ data_thread.start()
|
|
|
|
|
+ print(f"Flask 服务将在 http://0.0.0.0:5000 上运行 (刷新间隔: {REFRESH_INTERVAL_SECONDS}s)")
|
|
|
|
|
+ app.run(debug=False, host='0.0.0.0', port=5000, use_reloader=False)
|