浏览代码

sol debug完成,可以使用,加入中间汇率可以做USDT交易对

skyffire 6 月之前
父节点
当前提交
4094f6e3da
共有 2 个文件被更改,包括 234 次插入254 次删除
  1. 175 166
      price_checker.py
  2. 59 88
      templates/index_plotly_sol.html

+ 175 - 166
price_checker.py

@@ -3,30 +3,37 @@ import decimal
 import time
 import threading
 import json
-from flask import Flask, render_template, jsonify  # jsonify 仍然有用,但/data可能直接返回Plotly JSON字符串
+from flask import Flask, render_template, jsonify
 from collections import deque
 import logging
 
-import plotly  # 引入 Plotly
+import plotly
 import plotly.graph_objects as go
-from plotly.utils import PlotlyJSONEncoder  # 用于将Plotly图表序列化为JSON
-
-# --- 配置部分 (与之前相同) ---
-IN_TOKEN_ADDRESS_BSC = 'So11111111111111111111111111111111111111112'
-OUT_TOKEN_ADDRESS_BSC = 'C3DwDjT17gDvvCYC2nsdGHxDHVmQRdhKfpAdqQ29pump'  # out token
-AMOUNT_TO_QUERY_HUMAN = decimal.Decimal('1000')
-GATEIO_SPOT_PAIR = 'RFC_USDT'
-GATEIO_FUTURES_CONTRACT = 'RFC_USDT'
+from plotly.utils import PlotlyJSONEncoder
+
+# --- 配置部分 ---
+# OpenOcean (假设 'SOL' 是输入, 'RFC' 是输出, 得到 RFC/SOL 价格)
+IN_TOKEN_ADDRESS_SOLANA = 'So11111111111111111111111111111111111111112'  # SOL Mint Address
+OUT_TOKEN_ADDRESS_SOLANA = 'C3DwDjT17gDvvCYC2nsdGHxDHVmQRdhKfpAdqQ29pump'  # RFC Mint Address (示例)
+AMOUNT_TO_QUERY_OPENOCEAN_IN_SOL = decimal.Decimal('10')  # 例如用 10 SOL 去查询能买多少 RFC
+
+# Gate.io (RFC/USDT)
+GATEIO_SPOT_PAIR_RFC_USDT = 'RFC_USDT'
+GATEIO_FUTURES_CONTRACT_RFC_USDT = 'RFC_USDT'
+
+# Binance (SOL/USDT - 用于转换)
+BINANCE_SOL_USDT_PAIR = 'SOLUSDT'
+
 proxies = None
 decimal.getcontext().prec = 36
 
 
-# --- 价格获取函数 (与之前相同, 省略以保持简洁) ---
-def get_openocean_price_solana(in_token_addr, out_token_addr, human_amount_in_decimal_for_request, gas_price=3):
-    chain = 'solana';
+# --- 价格获取函数 ---
+def get_openocean_price_solana(in_token_addr, out_token_addr, human_amount_in_decimal_for_request):  # Solana gas is low
+    chain = 'solana'
     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}
+              'amount': str(human_amount_in_decimal_for_request), 'gasPrice': '0.000005'}
     try:
         r = requests.get(url, params=params, proxies=proxies, timeout=10);
         r.raise_for_status();
@@ -38,17 +45,20 @@ def get_openocean_price_solana(in_token_addr, out_token_addr, human_amount_in_de
                 '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}
+            h_in_sol = atomic_in / (10 ** in_dec)  # 这是花费的SOL数量
+            h_out_rfc = atomic_out / (10 ** out_dec)  # 这是得到的RFC数量
+            if h_out_rfc == 0: return {"error": "OO输出RFC为0"}
+            # 价格:1 RFC = X SOL  =>  SOL数量 / RFC数量
+            price_rfc_per_sol = h_in_sol / h_out_rfc
+            return {"price_sol_per_rfc": price_rfc_per_sol}  # 更明确的键名
         else:
             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:
-        return {"error": f"OO请求错误: {e}"}
+        return {"error": f"OO(Solana)请求错误: {e}"}
 
 
-def get_gateio_spot_price(pair_symbol):
+def get_gateio_spot_price_usdt(pair_symbol):  # 获取 RFC/USDT
     url = f'https://api.gateio.ws/api/v4/spot/tickers';
     params = {'currency_pair': pair_symbol}
     try:
@@ -58,18 +68,18 @@ def get_gateio_spot_price(pair_symbol):
         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'])}
+                return {"price_rfc_usdt": decimal.Decimal(td['last'])}
             else:
-                return {"error": f"Gate现货数据不匹配(Req:{pair_symbol},Res:{td.get('currency_pair')})或无价格"}
+                return {"error": f"Gate现货({pair_symbol})数据不匹配或无价格"}
         elif isinstance(data, dict) and data.get('label'):
-            return {"error": f"Gate现货API错误: {data.get('label')}-{data.get('message', '')}"}
+            return {"error": f"Gate现货API错误: {data['label']}-{data.get('message', '')}"}
         else:
-            return {"error": f"Gate现货API数据格式错误或未找到 {pair_symbol}"}
+            return {"error": f"Gate现货API({pair_symbol})数据格式错误或未找到"}
     except Exception as e:
-        return {"error": f"Gate现货请求错误: {e}"}
+        return {"error": f"Gate现货({pair_symbol})请求错误: {e}"}
 
 
-def get_gateio_futures_price(contract_symbol, settle_currency='usdt'):
+def get_gateio_futures_price_usdt(contract_symbol, settle_currency='usdt'):  # 获取 RFC/USDT
     url = f'https://api.gateio.ws/api/v4/futures/{settle_currency}/tickers';
     params = {'contract': contract_symbol}
     try:
@@ -79,226 +89,225 @@ def get_gateio_futures_price(contract_symbol, settle_currency='usdt'):
         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'])}
+                return {"price_rfc_usdt": decimal.Decimal(td['last'])}
             else:
-                return {"error": f"Gate期货数据不匹配(Req:{contract_symbol},Res:{td.get('contract')})或无价格"}
+                return {"error": f"Gate期货({contract_symbol})数据不匹配或无价格"}
         elif isinstance(data, dict) and data.get('label'):
-            return {"error": f"Gate期货API错误: {data.get('label')}-{data.get('message', '')}"}
+            return {"error": f"Gate期货API错误: {data['label']}-{data.get('message', '')}"}
+        else:
+            return {"error": f"Gate期货API({contract_symbol})数据格式错误或未找到"}
+    except Exception as e:
+        return {"error": f"Gate期货({contract_symbol})请求错误: {e}"}
+
+
+def get_binance_spot_price_usdt(symbol):  # 获取 SOL/USDT
+    # 使用币安现货API: https://binance-docs.github.io/apidocs/spot/en/#ticker-price
+    url = "https://api.binance.com/api/v3/ticker/price"
+    params = {'symbol': symbol}  # e.g., 'SOLUSDT'
+    try:
+        response = requests.get(url, params=params, proxies=proxies, timeout=10)
+        response.raise_for_status()
+        data = response.json()
+        if 'price' in data and data.get('symbol') == symbol:
+            return {"price_sol_usdt": decimal.Decimal(data['price'])}
         else:
-            return {"error": f"Gate期货API数据格式错误或未找到 {contract_symbol}"}
+            msg = data.get('msg', '未知错误')
+            return {"error": f"Binance API错误 ({symbol}): {msg}"}
+    except requests.exceptions.RequestException as e:
+        return {"error": f"Binance API ({symbol}) 请求失败: {e}"}
     except Exception as e:
-        return {"error": f"Gate期货请求错误: {e}"}
+        return {"error": f"Binance API ({symbol}) 意外错误: {e}"}
 
 
 app = Flask(__name__)
 log = logging.getLogger('werkzeug');
 log.setLevel(logging.ERROR)
 
-MAX_HISTORY_POINTS_PLOTLY = 300  # Plotly 图表显示的点数
-REFRESH_INTERVAL_SECONDS = 1  # 后端数据更新频率
+MAX_HISTORY_POINTS_PLOTLY = 300
+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,
+    "oo_rfc_sol_price": "N/A",  # OpenOcean RFC/SOL
+    "gate_spot_rfc_sol_price": "N/A",  # Gate Spot RFC/USDT converted to RFC/SOL
+    "gate_futures_rfc_sol_price": "N/A",  # Gate Futures RFC/USDT converted to RFC/SOL
+    "sol_usdt_price_binance": "N/A",  # For reference
+    "diff_oo_vs_spot_rfc_sol_percentage": "N/A",
+    "diff_oo_vs_futures_rfc_sol_percentage": "N/A",
+    "diff_spot_vs_futures_rfc_sol_percentage": "N/A",
+    "oo_error": None, "gate_spot_error": None, "gate_futures_error": None, "binance_sol_error": None,
     "last_updated": "N/A",
-    "gate_spot_pair_name": GATEIO_SPOT_PAIR,
-    "gate_futures_contract_name": GATEIO_FUTURES_CONTRACT,
-    "target_asset_symbol": GATEIO_SPOT_PAIR.split('_')[0]
+    "gate_spot_pair_name_usdt": GATEIO_SPOT_PAIR_RFC_USDT,
+    "gate_futures_contract_name_usdt": GATEIO_FUTURES_CONTRACT_RFC_USDT,
+    "target_asset_symbol": GATEIO_SPOT_PAIR_RFC_USDT.split('_')[0]  # RFC
 }
-data_lock = threading.Lock()  # 保护共享数据
+data_lock = threading.Lock()
 
 
 def calculate_percentage_diff(price_a, price_b):
     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:
+                                                                                                           decimal.Decimal) and price_b != 0:  # 基准不能为0
         return ((price_a - price_b) / price_b) * 100
     return None
 
 
+def convert_usdt_price_to_sol_price(price_asset_usdt, price_sol_usdt):
+    """Converts Price(Asset/USDT) to Price(Asset/SOL) using Price(SOL/USDT)"""
+    if price_asset_usdt is not None and price_sol_usdt is not None and price_sol_usdt > 0:
+        return price_asset_usdt / price_sol_usdt
+    return None
+
+
 def update_data_for_plotly_and_table():
     global historical_data_points, latest_values_for_table
     print("数据更新线程启动...")
     while True:
         fetch_time_full = time.strftime("%Y-%m-%d %H:%M:%S")
-        fetch_time_chart = time.strftime("%H:%M:%S")  # 或者使用 time.time() 获取更精确的unix时间戳
+        fetch_time_chart = time.strftime("%H:%M:%S")
 
-        oo_data = get_openocean_price_solana(IN_TOKEN_ADDRESS_BSC, OUT_TOKEN_ADDRESS_BSC, AMOUNT_TO_QUERY_HUMAN)
-        spot_data = get_gateio_spot_price(GATEIO_SPOT_PAIR)
-        futures_data = get_gateio_futures_price(GATEIO_FUTURES_CONTRACT)
+        oo_data = get_openocean_price_solana(IN_TOKEN_ADDRESS_SOLANA, OUT_TOKEN_ADDRESS_SOLANA,
+                                             AMOUNT_TO_QUERY_OPENOCEAN_IN_SOL)
+        spot_usdt_data = get_gateio_spot_price_usdt(GATEIO_SPOT_PAIR_RFC_USDT)
+        futures_usdt_data = get_gateio_futures_price_usdt(GATEIO_FUTURES_CONTRACT_RFC_USDT)
+        binance_sol_usdt_data = get_binance_spot_price_usdt(BINANCE_SOL_USDT_PAIR)
 
-        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_rfc_sol_price = oo_data.get("price_sol_per_rfc")  # 1 RFC = X SOL
+        spot_rfc_usdt_price = spot_usdt_data.get("price_rfc_usdt")  # 1 RFC = X USDT
+        futures_rfc_usdt_price = futures_usdt_data.get("price_rfc_usdt")  # 1 RFC = X USDT
+        sol_usdt_price = binance_sol_usdt_data.get("price_sol_usdt")  # 1 SOL = X USDT
 
         oo_err = oo_data.get("error")
-        spot_err = spot_data.get("error")
-        futures_err = futures_data.get("error")
+        spot_err = spot_usdt_data.get("error")
+        futures_err = futures_usdt_data.get("error")
+        binance_err = binance_sol_usdt_data.get("error")
 
-        diff_oo_spot_pct = calculate_percentage_diff(oo_price, spot_price)
-        diff_oo_futures_pct = calculate_percentage_diff(oo_price, futures_price)
-        diff_spot_futures_pct = calculate_percentage_diff(spot_price, futures_price)
+        # Convert Gate.io prices to RFC/SOL
+        spot_rfc_sol_price = convert_usdt_price_to_sol_price(spot_rfc_usdt_price, sol_usdt_price)
+        futures_rfc_sol_price = convert_usdt_price_to_sol_price(futures_rfc_usdt_price, sol_usdt_price)
+
+        # Calculate diffs based on RFC/SOL prices
+        diff_oo_spot_pct = calculate_percentage_diff(oo_rfc_sol_price, spot_rfc_sol_price)
+        diff_oo_futures_pct = calculate_percentage_diff(oo_rfc_sol_price, futures_rfc_sol_price)
+        diff_spot_futures_pct = calculate_percentage_diff(spot_rfc_sol_price, futures_rfc_sol_price)
 
         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,
+            "time": fetch_time_chart,
+            "oo_rfc_sol": float(oo_rfc_sol_price) if oo_rfc_sol_price else None,
+            "spot_rfc_sol": float(spot_rfc_sol_price) if spot_rfc_sol_price else None,
+            "futures_rfc_sol": float(futures_rfc_sol_price) if futures_rfc_sol_price else None,
+            "diff_oo_spot_rfc_sol": float(diff_oo_spot_pct) if diff_oo_spot_pct is not None else None,
+            "diff_oo_futures_rfc_sol": float(diff_oo_futures_pct) if diff_oo_futures_pct is not None else None,
+            "diff_spot_futures_rfc_sol": float(diff_spot_futures_pct) if diff_spot_futures_pct is not None else None,
         }
 
         with data_lock:
             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["oo_rfc_sol_price"] = str(oo_rfc_sol_price) if oo_rfc_sol_price else "N/A"
+            latest_values_for_table["gate_spot_rfc_sol_price"] = str(
+                spot_rfc_sol_price) if spot_rfc_sol_price else "N/A"
+            latest_values_for_table["gate_futures_rfc_sol_price"] = str(
+                futures_rfc_sol_price) if futures_rfc_sol_price else "N/A"
+            latest_values_for_table["sol_usdt_price_binance"] = str(sol_usdt_price) if sol_usdt_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_rfc_sol_percentage"] = f"{diff_oo_spot_pct:+.4f}%" if diff_oo_spot_pct is not None else "N/A"
             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_rfc_sol_percentage"] = f"{diff_oo_futures_pct:+.4f}%" if diff_oo_futures_pct is not None else "N/A"
             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_rfc_sol_percentage"] = f"{diff_spot_futures_pct:+.4f}%" if diff_spot_futures_pct is not None else "N/A"
+
             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["binance_sol_error"] = binance_err
             latest_values_for_table["last_updated"] = fetch_time_full
 
-        # 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'}")
+        print(
+            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'} | Fut_RFC/SOL:{'OK' if futures_rfc_sol_price else 'F'} | SOL/USDT:{'OK' if sol_usdt_price else 'F'}")
 
         time.sleep(REFRESH_INTERVAL_SECONDS)
 
 
 @app.route('/')
 def index_plotly():
-    # 将配置传递给模板,用于标题等
-    target_asset = GATEIO_SPOT_PAIR.split('_')[0]
-    return render_template('index_plotly.html',
+    target_asset = latest_values_for_table["target_asset_symbol"]  # RFC
+    return render_template('index_plotly_sol.html',  # Use a new template name to avoid confusion
                            target_asset=target_asset,
-                           spot_pair=GATEIO_SPOT_PAIR,
-                           futures_contract=GATEIO_FUTURES_CONTRACT,
+                           spot_pair_usdt=GATEIO_SPOT_PAIR_RFC_USDT,
+                           futures_contract_usdt=GATEIO_FUTURES_CONTRACT_RFC_USDT,
+                           sol_usdt_pair_binance=BINANCE_SOL_USDT_PAIR,
                            refresh_interval_ms=REFRESH_INTERVAL_SECONDS * 1000)
 
 
-@app.route('/table-data')  # 新增一个专门用于表格数据的API
+@app.route('/table-data')
 def get_table_data():
     with data_lock:
         return jsonify(latest_values_for_table)
 
 
-@app.route('/plotly-chart-data')  # 用于Plotly图表的JSON数据
+@app.route('/plotly-chart-data')
 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)
+        if not points:
+            fig = go.Figure();
+            fig.update_layout(title_text="暂无数据")
+            return jsonify({"price_chart": json.loads(json.dumps(fig, cls=PlotlyJSONEncoder)),
+                            "diff_chart": json.loads(json.dumps(fig, cls=PlotlyJSONEncoder))})
 
         times = [p['time'] for p in points]
+        target_asset_symbol = latest_values_for_table["target_asset_symbol"]  # RFC
 
-        # --- 创建价格图 (主图) ---
         fig_prices = go.Figure()
-        # 在这里为每个 trace 添加 hovertemplate
-        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)'),
-            hovertemplate='<b>OpenOcean</b><br>Time: %{x}<br>Price: %{y:.6f}<extra></extra>'  # .6f 表示6位小数
-        ))
-        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)'),
-            hovertemplate=f'<b>Gate Spot ({GATEIO_SPOT_PAIR})</b><br>Time: %{{x}}<br>Price: %{{y:.6f}}<extra></extra>'
-        ))
-        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)'),
-            hovertemplate=f'<b>Gate Futures ({GATEIO_FUTURES_CONTRACT})</b><br>Time: %{{x}}<br>Price: %{{y:.6f}}<extra></extra>'
-        ))
-
-        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='平台',
-            hovermode='x unified'  # 或者 'x', 'closest'
-        )
-
-        # --- 创建价差图 (副图) ---
+        fig_prices.add_trace(
+            go.Scatter(x=times, y=[p['oo_rfc_sol'] for p in points], mode='lines', name='OpenOcean (RFC/SOL)',
+                       line=dict(color='rgb(75, 192, 192)'),
+                       hovertemplate=f'<b>OpenOcean (RFC/SOL)</b><br>Time: %{{x}}<br>Price: %{{y:.8f}} SOL<extra></extra>'))
+        fig_prices.add_trace(go.Scatter(x=times, y=[p['spot_rfc_sol'] for p in points], mode='lines',
+                                        name=f'Gate Spot ({GATEIO_SPOT_PAIR_RFC_USDT} → RFC/SOL)',
+                                        line=dict(color='rgb(255, 99, 132)'),
+                                        hovertemplate=f'<b>Gate Spot (RFC/SOL)</b><br>Time: %{{x}}<br>Price: %{{y:.8f}} SOL<extra></extra>'))
+        fig_prices.add_trace(go.Scatter(x=times, y=[p['futures_rfc_sol'] for p in points], mode='lines',
+                                        name=f'Gate Futures ({GATEIO_FUTURES_CONTRACT_RFC_USDT} → RFC/SOL)',
+                                        line=dict(color='rgb(54, 162, 235)'),
+                                        hovertemplate=f'<b>Gate Futures (RFC/SOL)</b><br>Time: %{{x}}<br>Price: %{{y:.8f}} SOL<extra></extra>'))
+
+        fig_prices.update_layout(title_text=f'{target_asset_symbol}/SOL 价格历史', xaxis_title='时间',
+                                 yaxis_title=f'价格 (1 {target_asset_symbol} = X SOL)', legend_title_text='平台',
+                                 hovermode='x unified')
+
         fig_diffs = go.Figure()
-        # 在这里为每个 trace 添加 hovertemplate
-        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)'),
-            hovertemplate=f'<b>OO vs Spot ({GATEIO_SPOT_PAIR})</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'
-            # :+.4f 表示带符号4位小数
-        ))
-        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)'),
-            hovertemplate=f'<b>OO vs Futures ({GATEIO_FUTURES_CONTRACT})</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'
-        ))
-        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)'),
-            hovertemplate=f'<b>Spot ({GATEIO_SPOT_PAIR}) vs Futures ({GATEIO_FUTURES_CONTRACT})</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'
-        ))
-
-        fig_diffs.update_layout(
-            title_text='价差百分比历史',
-            xaxis_title='时间',
-            yaxis_title='价差 (%)',
-            legend_title_text='对比',
-            yaxis_zeroline=True,
-            hovermode='x unified'  # 统一显示同一X轴下所有trace的值
-        )
-
-        # Plotly 不直接支持通过 fig.to_json() 合并具有完全独立Y轴的子图到一个figure中并保持良好交互性
-        # 最简单的方式是前端请求两个独立的图表JSON,然后分别渲染。
-        # 或者,我们可以使用 make_subplots,但共享X轴是关键。
-
-        # 为了简化前端,我们这里返回两个独立的Figure的JSON。前端需要能处理这个结构。
-        # 或者,更常见的是,前端请求两个不同的API端点来获取图表数据。
-        # 这里,我们将返回一个包含两个图表定义的字典。
+        fig_diffs.add_trace(go.Scatter(x=times, y=[p['diff_oo_spot_rfc_sol'] for p in points], mode='lines',
+                                       name=f'OO vs Spot (RFC/SOL)', line=dict(color='rgb(255, 159, 64)'),
+                                       hovertemplate=f'<b>OO vs Spot (RFC/SOL)</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'))
+        fig_diffs.add_trace(go.Scatter(x=times, y=[p['diff_oo_futures_rfc_sol'] for p in points], mode='lines',
+                                       name=f'OO vs Futures (RFC/SOL)', line=dict(color='rgb(153, 102, 255)'),
+                                       hovertemplate=f'<b>OO vs Futures (RFC/SOL)</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'))
+        fig_diffs.add_trace(go.Scatter(x=times, y=[p['diff_spot_futures_rfc_sol'] for p in points], mode='lines',
+                                       name=f'Spot vs Futures (RFC/SOL)', line=dict(color='rgb(75, 192, 75)'),
+                                       hovertemplate=f'<b>Spot vs Futures (RFC/SOL)</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'))
+
+        fig_diffs.update_layout(title_text=f'{target_asset_symbol}/SOL 价差百分比历史', xaxis_title='时间',
+                                yaxis_title='价差 (%)', legend_title_text='对比', yaxis_zeroline=True,
+                                hovermode='x unified')
 
         combined_figure_data = {
-            "price_chart": json.loads(json.dumps(fig_prices, cls=PlotlyJSONEncoder)),  # 确保可JSON序列化
+            "price_chart": json.loads(json.dumps(fig_prices, cls=PlotlyJSONEncoder)),
             "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder))
         }
-        return jsonify(combined_figure_data)  # jsonify 会自动处理JSON序列化
+        return jsonify(combined_figure_data)
 
 
 if __name__ == "__main__":
     print("应用启动...")
-    print(f"监控 OpenOcean (BSC代币: ...{OUT_TOKEN_ADDRESS_BSC[-6:]}) vs USDT")
-    print(f"Gate.io 现货: {GATEIO_SPOT_PAIR}")
-    print(f"Gate.io 期货: {GATEIO_FUTURES_CONTRACT}")
-    # ... (启动时的配置检查可以保留) ...
-    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}')不匹配!")
+    asset_symbol = GATEIO_SPOT_PAIR_RFC_USDT.split('_')[0]  # RFC
+    print(f"目标资产: {asset_symbol}")
+    print(
+        f"OpenOcean: {asset_symbol}/SOL (通过 {IN_TOKEN_ADDRESS_SOLANA[-6:]}...SOL / {OUT_TOKEN_ADDRESS_SOLANA[-6:]}...{asset_symbol})")
+    print(f"Gate.io 现货: {GATEIO_SPOT_PAIR_RFC_USDT} (将转换为 {asset_symbol}/SOL)")
+    print(f"Gate.io 期货: {GATEIO_FUTURES_CONTRACT_RFC_USDT} (将转换为 {asset_symbol}/SOL)")
+    print(f"币安转换汇率: {BINANCE_SOL_USDT_PAIR}")
 
     data_thread = threading.Thread(target=update_data_for_plotly_and_table, daemon=True)
     data_thread.start()

+ 59 - 88
templates/index_plotly.html → templates/index_plotly_sol.html

@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>多平台价格与价差监控 (Plotly)</title>
+    <title>{{ target_asset }}/SOL 多平台价格监控 (Plotly)</title>
     <script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
     <style>
         body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
@@ -17,7 +17,7 @@
         .error-message { color: #c00; font-style: italic;}
         .status-cell { min-width: 100px; }
         .timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 10px; }
-        .chart-container { width: 95%; height: 450px; margin: 20px auto; } /* Plotly chart div */
+        .chart-container { width: 95%; height: 450px; margin: 20px auto; }
         .controls-container { text-align: center; margin-bottom: 15px; margin-top: 5px; }
         .control-button { background-color: #007bff; color: white; border: none; padding: 8px 15px; font-size: 14px; border-radius: 5px; cursor: pointer; margin:0 5px; }
         .control-button:hover { background-color: #0056b3; }
@@ -30,7 +30,7 @@
 </head>
 <body>
     <div class="container">
-        <h1 id="main-title">{{ target_asset }}/USDT 多平台价格监控</h1>
+        <h1 id="main-title">{{ target_asset }}/SOL 多平台价格监控</h1>
         <div class="controls-container">
             <button id="pause-resume-button" class="control-button">暂停刷新</button>
         </div>
@@ -38,29 +38,34 @@
             <thead>
                 <tr>
                     <th>平台</th>
-                    <th>价格 (USDT)</th>
+                    <th>价格 (SOL)</th>
                     <th class="status-cell">状态/错误</th>
                 </tr>
             </thead>
             <tbody>
                 <tr>
-                    <td class="platform-name">OpenOcean (Chain)</td>
-                    <td id="oo-price">加载中...</td>
+                    <td class="platform-name">OpenOcean ({{ target_asset }}/SOL)</td>
+                    <td id="oo-rfc-sol-price">加载中...</td>
                     <td id="oo-status" class="status-cell"></td>
                 </tr>
                 <tr>
-                    <td class="platform-name" id="gate-spot-label">Gate.io 现货 ({{ spot_pair }})</td>
-                    <td id="gate-spot-price">加载中...</td>
+                    <td class="platform-name" id="gate-spot-label">Gate.io 现货 ({{ spot_pair_usdt }} → {{ target_asset }}/SOL)</td>
+                    <td id="gate-spot-rfc-sol-price">加载中...</td>
                     <td id="gate-spot-status" class="status-cell"></td>
                 </tr>
                 <tr>
-                    <td class="platform-name" id="gate-futures-label">Gate.io 期货 ({{ futures_contract }})</td>
-                    <td id="gate-futures-price">加载中...</td>
+                    <td class="platform-name" id="gate-futures-label">Gate.io 期货 ({{ futures_contract_usdt }} → {{ target_asset }}/SOL)</td>
+                    <td id="gate-futures-rfc-sol-price">加载中...</td>
                     <td id="gate-futures-status" class="status-cell"></td>
                 </tr>
+                <tr>
+                    <td class="platform-name">参考汇率 (Binance)</td>
+                    <td id="sol-usdt-price-binance" title="{{ sol_usdt_pair_binance }}">加载中... (USDT)</td>
+                    <td id="binance-sol-status" class="status-cell"></td>
+                </tr>
             </tbody>
         </table>
-        <h2>价差百分比</h2>
+        <h2>价差百分比 (基于 {{ target_asset }}/SOL 价格)</h2>
         <table>
              <thead>
                 <tr>
@@ -70,16 +75,16 @@
             </thead>
             <tbody>
                 <tr>
-                    <td id="diff-label-oo-spot">OO vs Gate.io 现货 ({{ spot_pair }})</td>
-                    <td id="diff-oo-vs-spot">计算中...</td>
+                    <td id="diff-label-oo-spot">OO vs Gate.io 现货 (转换后)</td>
+                    <td id="diff-oo-vs-spot-rfc-sol">计算中...</td>
                 </tr>
                 <tr>
-                    <td id="diff-label-oo-futures">OO vs Gate.io 期货 ({{ futures_contract }})</td>
-                    <td id="diff-oo-vs-futures">计算中...</td>
+                    <td id="diff-label-oo-futures">OO vs Gate.io 期货 (转换后)</td>
+                    <td id="diff-oo-vs-futures-rfc-sol">计算中...</td>
                 </tr>
                 <tr>
-                    <td id="diff-label-spot-futures">Gate.io 现货 ({{ spot_pair }}) vs 期货 ({{ futures_contract }})</td>
-                    <td id="diff-spot-vs-futures">计算中...</td>
+                    <td id="diff-label-spot-futures">Gate.io 现货 vs 期货 (转换后)</td>
+                    <td id="diff-spot-vs-futures-rfc-sol">计算中...</td>
                 </tr>
             </tbody>
         </table>
@@ -87,13 +92,13 @@
     </div>
 
     <div class="container">
-        <h2 id="price-chart-title">价格历史曲线</h2>
+        <h2 id="price-chart-title">{{ target_asset }}/SOL 价格历史曲线</h2>
         <div id='priceHistoryChartPlotly' class="chart-container"></div>
         <div id="price-chart-status" class="status-line">加载价格图表...</div>
     </div>
 
     <div class="container">
-        <h2 id="diff-chart-title">价差百分比历史曲线</h2>
+        <h2 id="diff-chart-title">{{ target_asset }}/SOL 价差百分比历史曲线</h2>
         <div id='diffPercentageChartPlotly' class="chart-container"></div>
         <div id="diff-chart-status" class="status-line">加载价差图表...</div>
     </div>
@@ -112,51 +117,51 @@
         let pricePlotInitialized = false;
         let diffPlotInitialized = false;
 
-        // --- Helper function to format price (same as your Chart.js version) ---
-        function formatPriceForTable(priceStr) {
-            if (priceStr === null || priceStr === undefined || priceStr === "N/A" || String(priceStr).toLowerCase() === "n/a") return "N/A";
+        function formatPriceForTable(priceStr, precision = 8) { // Default precision 8 for SOL prices
+            if (priceStr === null || priceStr === undefined || String(priceStr).toLowerCase() === "n/a") return "N/A";
             const price = parseFloat(priceStr);
             if (isNaN(price)) return "N/A";
-            if (price === 0) return "0.00000000";
-            if (Math.abs(price) < 0.0000001 && price !== 0) return price.toExponential(3);
-            if (Math.abs(price) < 0.001) return price.toFixed(8);
-            if (Math.abs(price) < 1) return price.toFixed(6);
-            return price.toFixed(4);
+            if (price === 0) return (0).toFixed(precision);
+            if (Math.abs(price) < 1e-9 && price !== 0) return price.toExponential(3); // Very small numbers
+            return price.toFixed(precision);
         }
         function formatPercentageForTable(percStr) {
-            if (percStr === null || percStr === undefined || percStr === "N/A" || String(percStr).toLowerCase() === "n/a") return "N/A";
-            if (String(percStr).includes('%')) return percStr; // Already formatted
+            if (percStr === null || percStr === undefined || String(percStr).toLowerCase() === "n/a") return "N/A";
+            if (String(percStr).includes('%')) return percStr;
             const perc = parseFloat(percStr);
             if (isNaN(perc)) return "N/A";
             return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`;
         }
 
         async function updateTableData() {
-            if (isPaused) return; // Don't update table if paused (charts are also paused via their own update)
+            if (isPaused) return;
             try {
                 const response = await fetch('/table-data');
                 if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                 const data = await response.json();
 
-                document.getElementById('oo-price').textContent = formatPriceForTable(data.oo_price);
-                document.getElementById('gate-spot-price').textContent = formatPriceForTable(data.gate_spot_price);
-                document.getElementById('gate-futures-price').textContent = formatPriceForTable(data.gate_futures_price);
+                document.getElementById('oo-rfc-sol-price').textContent = formatPriceForTable(data.oo_rfc_sol_price);
+                document.getElementById('gate-spot-rfc-sol-price').textContent = formatPriceForTable(data.gate_spot_rfc_sol_price);
+                document.getElementById('gate-futures-rfc-sol-price').textContent = formatPriceForTable(data.gate_futures_rfc_sol_price);
+                document.getElementById('sol-usdt-price-binance').textContent = formatPriceForTable(data.sol_usdt_price_binance, 4); // SOL/USDT with 4 decimals
 
                 document.getElementById('oo-status').textContent = data.oo_error || '正常';
                 document.getElementById('gate-spot-status').textContent = data.gate_spot_error || '正常';
                 document.getElementById('gate-futures-status').textContent = data.gate_futures_error || '正常';
-                // Update error classes
+                document.getElementById('binance-sol-status').textContent = data.binance_sol_error || '正常';
+
                 document.getElementById('oo-status').className = data.oo_error ? 'status-cell error-message' : 'status-cell';
                 document.getElementById('gate-spot-status').className = data.gate_spot_error ? 'status-cell error-message' : 'status-cell';
                 document.getElementById('gate-futures-status').className = data.gate_futures_error ? 'status-cell error-message' : 'status-cell';
+                document.getElementById('binance-sol-status').className = data.binance_sol_error ? 'status-cell error-message' : 'status-cell';
 
-                const diffOOSpotEl = document.getElementById('diff-oo-vs-spot');
-                const diffOOFuturesEl = document.getElementById('diff-oo-vs-futures');
-                const diffSpotFuturesEl = document.getElementById('diff-spot-vs-futures');
+                const diffOOSpotEl = document.getElementById('diff-oo-vs-spot-rfc-sol');
+                const diffOOFuturesEl = document.getElementById('diff-oo-vs-futures-rfc-sol');
+                const diffSpotFuturesEl = document.getElementById('diff-spot-vs-futures-rfc-sol');
 
-                diffOOSpotEl.textContent = formatPercentageForTable(data.diff_oo_vs_spot_percentage);
-                diffOOFuturesEl.textContent = formatPercentageForTable(data.diff_oo_vs_futures_percentage);
-                diffSpotFuturesEl.textContent = formatPercentageForTable(data.diff_spot_vs_futures_percentage);
+                diffOOSpotEl.textContent = formatPercentageForTable(data.diff_oo_vs_spot_rfc_sol_percentage);
+                diffOOFuturesEl.textContent = formatPercentageForTable(data.diff_oo_vs_futures_rfc_sol_percentage);
+                diffSpotFuturesEl.textContent = formatPercentageForTable(data.diff_spot_vs_futures_rfc_sol_percentage);
 
                 [diffOOSpotEl, diffOOFuturesEl, diffSpotFuturesEl].forEach(el => {
                     const valStr = el.textContent.replace('%','').replace('+','');
@@ -169,42 +174,27 @@
 
             } catch (error) {
                 console.error('Error fetching table data:', error);
-                // Optionally update status for table data errors
             }
         }
 
         async function updatePlotlyCharts() {
-            if(isPaused && (pricePlotInitialized || diffPlotInitialized) ) return; // Don't fetch new chart data if paused and already drawn once
-
+            if(isPaused && (pricePlotInitialized || diffPlotInitialized) ) return;
             try {
                 const response = await fetch('/plotly-chart-data');
                 if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
-
-                const chartData = await response.json(); // This now contains { price_chart: ..., diff_chart: ... }
+                const chartData = await response.json();
 
                 if (chartData.price_chart && chartData.price_chart.data && chartData.price_chart.layout) {
                     Plotly.react(priceChartDiv, chartData.price_chart.data, chartData.price_chart.layout, {responsive: true});
-                    if (!pricePlotInitialized) {
-                        console.log("Price chart initialized with Plotly.");
-                        pricePlotInitialized = true;
-                    }
+                    if (!pricePlotInitialized) pricePlotInitialized = true;
                     priceChartStatusDiv.textContent = `价格图表更新于: ${new Date().toLocaleTimeString()}`;
-                } else {
-                    console.error("Received invalid price_chart data structure");
-                    priceChartStatusDiv.textContent = "错误: 价格图表数据无效。";
-                }
+                } else { priceChartStatusDiv.textContent = "错误: 价格图表数据无效。"; }
 
                 if (chartData.diff_chart && chartData.diff_chart.data && chartData.diff_chart.layout) {
                     Plotly.react(diffChartDiv, chartData.diff_chart.data, chartData.diff_chart.layout, {responsive: true});
-                     if (!diffPlotInitialized) {
-                        console.log("Diff chart initialized with Plotly.");
-                        diffPlotInitialized = true;
-                    }
+                     if (!diffPlotInitialized) diffPlotInitialized = true;
                     diffChartStatusDiv.textContent = `价差图表更新于: ${new Date().toLocaleTimeString()}`;
-                } else {
-                    console.error("Received invalid diff_chart data structure");
-                    diffChartStatusDiv.textContent = "错误: 价差图表数据无效。";
-                }
+                } else { diffChartStatusDiv.textContent = "错误: 价差图表数据无效。"; }
 
             } catch (error) {
                 console.error('Error fetching or plotting Plotly data:', error);
@@ -216,39 +206,20 @@
         function togglePauseResume() {
             isPaused = !isPaused;
             if (isPaused) {
-                if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID);
-                dataUpdateIntervalID = null;
-                pauseResumeButton.textContent = '继续刷新';
-                pauseResumeButton.classList.add('pause-button-active');
-                console.log("Data refresh PAUSED");
+                if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID); dataUpdateIntervalID = null;
+                pauseResumeButton.textContent = '继续刷新'; pauseResumeButton.classList.add('pause-button-active');
             } else {
-                pauseResumeButton.textContent = '暂停刷新';
-                pauseResumeButton.classList.remove('pause-button-active');
-                // Refresh immediately on resume
-                updateTableData();
-                updatePlotlyCharts();
-                dataUpdateIntervalID = setInterval(() => {
-                    updateTableData();
-                    updatePlotlyCharts();
-                }, refreshIntervalMs);
-                console.log("Data refresh RESUMED");
+                pauseResumeButton.textContent = '暂停刷新'; pauseResumeButton.classList.remove('pause-button-active');
+                updateTableData(); updatePlotlyCharts();
+                dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs);
             }
         }
-
         pauseResumeButton.addEventListener('click', togglePauseResume);
 
-        // Initial load
-        console.log("Page loaded. Initializing data and charts...");
-        updateTableData();
-        updatePlotlyCharts();
-
+        updateTableData(); updatePlotlyCharts();
         if (!isPaused) {
-            dataUpdateIntervalID = setInterval(() => {
-                updateTableData();
-                updatePlotlyCharts();
-            }, refreshIntervalMs);
+            dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs);
         }
-
     </script>
 </body>
-</html>
+</html>