Parcourir la source

图例放上面

skyffire il y a 6 mois
Parent
commit
89eea649cd
2 fichiers modifiés avec 90 ajouts et 213 suppressions
  1. 45 95
      price_checker.py
  2. 45 118
      templates/index_plotly_sol_spot_only.html

+ 45 - 95
price_checker.py

@@ -12,22 +12,18 @@ import plotly.graph_objects as go
 from plotly.utils import PlotlyJSONEncoder
 
 # --- 配置部分 ---
-IN_TOKEN_ADDRESS_SOLANA = 'So11111111111111111111111111111111111111112'  # SOL Mint Address
-OUT_TOKEN_ADDRESS_SOLANA = '38PgzpJYu2HkiYvV8qePFakB8tuobPdGm2FFEn7Dpump'  #OutToken
+IN_TOKEN_ADDRESS_SOLANA = 'So11111111111111111111111111111111111111112'
+OUT_TOKEN_ADDRESS_SOLANA = 'ED5nyyWEzpPPiWimP8vYm7sD7TD3LAt3Q3gRTWHzPJBY'
 AMOUNT_TO_QUERY_OPENOCEAN_IN_SOL = decimal.Decimal('10')
-
-GATEIO_SPOT_PAIR_RFC_USDT = 'GORK_USDT'
-# GATEIO_FUTURES_CONTRACT_RFC_USDT = 'GORK_USDT' # 注释掉期货配置
-
+GATEIO_SPOT_PAIR_RFC_USDT = 'MOODENG_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):
-    chain = 'solana'
+    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': '0.000005'}
@@ -40,13 +36,12 @@ def get_openocean_price_solana(in_token_addr, out_token_addr, human_amount_in_de
             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'])
+            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_sol = atomic_in / (10 ** in_dec);
             h_out_rfc = atomic_out / (10 ** out_dec)
             if h_out_rfc == 0: return {"error": "OO输出RFC为0"}
-            price_rfc_per_sol = h_in_sol / h_out_rfc
-            return {"price_sol_per_rfc": price_rfc_per_sol}
+            return {"price_sol_per_rfc": h_in_sol / h_out_rfc}
         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 '格式错误'}"}
@@ -63,7 +58,7 @@ def get_gateio_spot_price_usdt(pair_symbol):
         data = r.json()
         if isinstance(data, list) and data:
             td = data[0]
-            if td.get('currency_pair') == pair_symbol and td.get('lowest_ask'):  # 使用 lowest_ask 作为卖价
+            if td.get('currency_pair') == pair_symbol and td.get('lowest_ask'):
                 return {"price_rfc_usdt": decimal.Decimal(td['lowest_ask'])}
             else:
                 return {"error": f"Gate现货({pair_symbol})数据不匹配或无价格"}
@@ -75,19 +70,6 @@ def get_gateio_spot_price_usdt(pair_symbol):
         return {"error": f"Gate现货({pair_symbol})请求错误: {e}"}
 
 
-# def get_gateio_futures_price_usdt(contract_symbol, settle_currency='usdt'): # 注释掉期货函数
-#     url = f'https://api.gateio.ws/api/v4/futures/{settle_currency}/tickers'; params = {'contract': contract_symbol}
-#     try:
-#         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('highest_bid'): # 使用 highest_bid 作为买价
-#                 return {"price_rfc_usdt": decimal.Decimal(td['highest_bid'])}
-#             else: return {"error": f"Gate期货({contract_symbol})数据不匹配或无价格"}
-#         elif isinstance(data, dict) and data.get('label'): 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):
     url = "https://api.binance.com/api/v3/ticker/price";
     params = {'symbol': symbol}
@@ -109,24 +91,15 @@ app = Flask(__name__)
 log = logging.getLogger('werkzeug');
 log.setLevel(logging.ERROR)
 
-MAX_HISTORY_POINTS_PLOTLY = 86400  # 你坚持要这个值,保留它,但请注意性能
+MAX_HISTORY_POINTS_PLOTLY = 86400
 REFRESH_INTERVAL_SECONDS = 1
 
 historical_data_points = deque(maxlen=MAX_HISTORY_POINTS_PLOTLY)
 latest_values_for_table = {
-    "oo_rfc_sol_price": "N/A",
-    "gate_spot_rfc_sol_price": "N/A",
-    # "gate_futures_rfc_sol_price": "N/A", # 注释
-    "sol_usdt_price_binance": "N/A",
+    "oo_rfc_sol_price": "N/A", "gate_spot_rfc_sol_price": "N/A", "sol_usdt_price_binance": "N/A",
     "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_usdt": GATEIO_SPOT_PAIR_RFC_USDT,
-    # "gate_futures_contract_name_usdt": GATEIO_FUTURES_CONTRACT_RFC_USDT, # 注释
+    "oo_error": None, "gate_spot_error": None, "binance_sol_error": None,
+    "last_updated": "N/A", "gate_spot_pair_name_usdt": GATEIO_SPOT_PAIR_RFC_USDT,
     "target_asset_symbol": GATEIO_SPOT_PAIR_RFC_USDT.split('_')[0]
 }
 data_lock = threading.Lock()
@@ -149,62 +122,38 @@ 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_full = time.strftime("%Y-%m-%d %H:%M:%S");
         fetch_time_chart = time.strftime("%H:%M:%S")
-
         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_rfc_sol_price = oo_data.get("price_sol_per_rfc")
+        oo_rfc_sol_price = oo_data.get("price_sol_per_rfc");
         spot_rfc_usdt_price = spot_usdt_data.get("price_rfc_usdt")
-        # futures_rfc_usdt_price = futures_usdt_data.get("price_rfc_usdt") # 注释
         sol_usdt_price = binance_sol_usdt_data.get("price_sol_usdt")
-
-        oo_err = oo_data.get("error")
-        spot_err = spot_usdt_data.get("error")
-        # futures_err = futures_usdt_data.get("error") # 注释
+        oo_err = oo_data.get("error");
+        spot_err = spot_usdt_data.get("error");
         binance_err = binance_sol_usdt_data.get("error")
-
         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) # 注释
-
         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,
             "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_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_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_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_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["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["binance_sol_error"] = binance_err;
             latest_values_for_table["last_updated"] = fetch_time_full
-
         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'} | SOL/USDT:{'OK' if sol_usdt_price else 'F'}")
         time.sleep(REFRESH_INTERVAL_SECONDS)
@@ -213,17 +162,14 @@ def update_data_for_plotly_and_table():
 @app.route('/')
 def index_plotly():
     target_asset = latest_values_for_table["target_asset_symbol"]
-    return render_template('index_plotly_sol_spot_only.html',
-                           target_asset=target_asset,
-                           spot_pair_usdt=GATEIO_SPOT_PAIR_RFC_USDT,
-                           sol_usdt_pair_binance=BINANCE_SOL_USDT_PAIR,
+    return render_template('index_plotly_sol_spot_only.html', target_asset=target_asset,
+                           spot_pair_usdt=GATEIO_SPOT_PAIR_RFC_USDT, sol_usdt_pair_binance=BINANCE_SOL_USDT_PAIR,
                            refresh_interval_ms=REFRESH_INTERVAL_SECONDS * 1000)
 
 
 @app.route('/table-data')
 def get_table_data():
-    with data_lock:
-        return jsonify(latest_values_for_table)
+    with data_lock: return jsonify(latest_values_for_table)
 
 
 @app.route('/plotly-chart-data')
@@ -233,12 +179,20 @@ def get_plotly_chart_data():
         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))})
+            empty_json = json.loads(json.dumps(fig, cls=PlotlyJSONEncoder))
+            return jsonify({"price_chart": empty_json, "diff_chart": empty_json})
 
         times = [p['time'] for p in points]
         target_asset_symbol = latest_values_for_table["target_asset_symbol"]
 
+        common_xaxis_config = dict(title='时间')
+        if len(times) > 1:
+            common_xaxis_config['range'] = [times[0], times[-1]]
+        else:
+            common_xaxis_config['autorange'] = True
+
+        common_legend_config = dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
+
         fig_prices = go.Figure()
         fig_prices.add_trace(
             go.Scatter(x=times, y=[p['oo_rfc_sol'] for p in points], mode='lines', name='OpenOcean (RFC/SOL)',
@@ -248,27 +202,25 @@ def get_plotly_chart_data():
                                         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='时间',
+        fig_prices.update_layout(title_text=f'{target_asset_symbol}/SOL 价格历史', xaxis=common_xaxis_config.copy(),
                                  yaxis_title=f'价格 (1 {target_asset_symbol} = X SOL)', legend_title_text='平台',
-                                 hovermode='x unified')
+                                 legend=common_legend_config.copy(), hovermode='x unified',
+                                 margin=dict(l=70, r=30, t=80, b=50))
 
         fig_diffs = go.Figure()
         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 价差百分比历史 (OO vs Spot)', 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)),
-            "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder))
-        }
+        fig_diffs.update_layout(title_text=f'{target_asset_symbol}/SOL 价差百分比历史 (OO vs Spot)',
+                                xaxis=common_xaxis_config.copy(),
+                                yaxis_title='价差 (%)',
+                                legend=common_legend_config.copy(),
+                                # Applying to diff chart as well, or set showlegend=False
+                                # showlegend=False, # Uncomment this if you don't want legend on diff chart
+                                yaxis_zeroline=True, hovermode='x unified', margin=dict(l=70, r=30, t=80, b=50))
+
+        combined_figure_data = {"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)
 
 
@@ -279,10 +231,8 @@ if __name__ == "__main__":
     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 = 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)

+ 45 - 118
templates/index_plotly_sol_spot_only.html

@@ -6,7 +6,6 @@
     <title>{{ target_asset }}/SOL 价格监控 (Plotly - 现货Only)</title>
     <script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
     <style>
-        /* CSS样式保持不变 */
         body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
         .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
         h1, h2 { text-align: center; color: #333; margin-top:10px; margin-bottom:15px; }
@@ -18,7 +17,8 @@
         .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; }
+        .chart-container { width: 95%; height: 400px; margin: 20px auto; } /* 主图默认高度 */
+        .diff-chart-container { height: 280px; } /* 副图特定高度 */
         .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; }
@@ -26,7 +26,7 @@
         .platform-name {font-weight: bold;}
         #main-title { font-size: 1.8em; }
         h2 { font-size: 1.3em; }
-        .status-line { text-align: center; margin-top: 10px; font-style: italic; color: grey; }
+        .status-line { text-align: center; margin-top: 10px; font-size:0.8em; font-style: italic; color: grey; }
     </style>
 </head>
 <body>
@@ -36,47 +36,17 @@
             <button id="pause-resume-button" class="control-button">暂停刷新</button>
         </div>
         <table>
-            <thead>
-                <tr>
-                    <th>平台</th>
-                    <th>价格 (SOL)</th>
-                    <th class="status-cell">状态/错误</th>
-                </tr>
-            </thead>
+            <thead> <tr><th>平台</th><th>价格 (SOL)</th><th class="status-cell">状态/错误</th></tr> </thead>
             <tbody>
-                <tr>
-                    <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_usdt }} → {{ target_asset }}/SOL)</td>
-                    <td id="gate-spot-rfc-sol-price">加载中...</td>
-                    <td id="gate-spot-status" class="status-cell"></td>
-                </tr>
-                <!-- Gate.io 期货行被移除 -->
-                <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>
+                <tr><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_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">参考汇率 (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>价差百分比 (基于 {{ target_asset }}/SOL 价格)</h2>
         <table>
-             <thead>
-                <tr>
-                    <th>对比</th>
-                    <th>价差 (%)</th>
-                </tr>
-            </thead>
-            <tbody>
-                <tr>
-                    <td id="diff-label-oo-spot">OO vs Gate.io 现货 (转换后)</td>
-                    <td id="diff-oo-vs-spot-rfc-sol">计算中...</td>
-                </tr>
-                <!-- 其他价差行被移除 -->
-            </tbody>
+             <thead><tr><th>对比</th><th>价差 (%)</th></tr></thead>
+            <tbody><tr><td id="diff-label-oo-spot">OO vs Gate.io 现货 (转换后)</td><td id="diff-oo-vs-spot-rfc-sol">计算中...</td></tr></tbody>
         </table>
         <div class="timestamp">最后更新: <span id="last-updated">N/A</span></div>
     </div>
@@ -89,7 +59,7 @@
 
     <div class="container">
         <h2 id="diff-chart-title">{{ target_asset }}/SOL 价差百分比历史曲线 (OO vs Spot)</h2>
-        <div id='diffPercentageChartPlotly' class="chart-container"></div>
+        <div id='diffPercentageChartPlotly' class="chart-container diff-chart-container"></div> <!-- Applied diff-chart-container class -->
         <div id="diff-chart-status" class="status-line">加载价差图表...</div>
     </div>
 
@@ -103,16 +73,13 @@
         const refreshIntervalMs = {{ refresh_interval_ms }};
         let dataUpdateIntervalID = null;
         let isPaused = false;
-
         let pricePlotInitialized = false;
         let diffPlotInitialized = false;
         let isSyncingLayout = false;
 
-        // --- Helper functions ---
         function formatPriceForTable(priceStr, precision = 8) {
             if (priceStr === null || priceStr === undefined || String(priceStr).toLowerCase() === "n/a") return "N/A";
-            const price = parseFloat(priceStr);
-            if (isNaN(price)) return "N/A";
+            const price = parseFloat(priceStr); if (isNaN(price)) return "N/A";
             if (price === 0) return (0).toFixed(precision);
             if (Math.abs(price) < 1e-9 && price !== 0) return price.toExponential(3);
             return price.toFixed(precision);
@@ -120,125 +87,85 @@
         function formatPercentageForTable(percStr) {
             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";
+            const perc = parseFloat(percStr); if (isNaN(perc)) return "N/A";
             return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`;
         }
 
-        // --- Function to sync X-axis ---
         function syncPlotlyXAxes(sourceDiv, targetDiv, eventData) {
-            if (isSyncingLayout) return;
-            isSyncingLayout = true;
-            const update = {};
-            if (eventData && eventData['xaxis.range[0]'] !== undefined) {
-                update['xaxis.range[0]'] = eventData['xaxis.range[0]'];
-                update['xaxis.range[1]'] = eventData['xaxis.range[1]'];
-            } else if (eventData && eventData['xaxis.autorange']) {
-                update['xaxis.autorange'] = true;
+            if (isSyncingLayout) return; isSyncingLayout = true;
+            const update = {}; let newXRange = null;
+            if (eventData && eventData['xaxis.autorange'] === true) { update['xaxis.autorange'] = true; }
+            else if (eventData && eventData['xaxis.range[0]'] !== undefined && eventData['xaxis.range[1]'] !== undefined) {
+                newXRange = [eventData['xaxis.range[0]'], eventData['xaxis.range[1]']];
+                update['xaxis.range'] = newXRange; update['xaxis.autorange'] = false;
             } else {
-                if (sourceDiv.layout && sourceDiv.layout.xaxis && sourceDiv.layout.xaxis.range) {
-                     update['xaxis.range[0]'] = sourceDiv.layout.xaxis.range[0];
-                     update['xaxis.range[1]'] = sourceDiv.layout.xaxis.range[1];
+                if (sourceDiv.layout && sourceDiv.layout.xaxis) {
+                    if (sourceDiv.layout.xaxis.autorange) { update['xaxis.autorange'] = true; }
+                    else if (sourceDiv.layout.xaxis.range) { newXRange = sourceDiv.layout.xaxis.range; update['xaxis.range'] = newXRange; update['xaxis.autorange'] = false; }
+                    else { isSyncingLayout = false; return; }
                 } else { isSyncingLayout = false; return; }
             }
-            if (Object.keys(update).length > 0) {
-                Plotly.relayout(targetDiv, update).then(() => { isSyncingLayout = false; }).catch(e => { console.error("Error syncing layout:", e); isSyncingLayout = false;});
-            } else { isSyncingLayout = false; }
+            Plotly.relayout(targetDiv, update).then(() => { isSyncingLayout = false; }).catch(e => { console.error("Error syncing layout:", e); isSyncingLayout = false; });
         }
 
-        // --- Update Table Data ---
         async function updateTableData() {
             if (isPaused) return;
             try {
-                const response = await fetch('/table-data');
-                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+                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-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); // Removed
                 document.getElementById('sol-usdt-price-binance').textContent = formatPriceForTable(data.sol_usdt_price_binance, 4);
-
                 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 || '正常'; // Removed
                 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'; // Removed
                 document.getElementById('binance-sol-status').className = data.binance_sol_error ? 'status-cell error-message' : 'status-cell';
-
                 const diffOOSpotEl = document.getElementById('diff-oo-vs-spot-rfc-sol');
-                // const diffOOFuturesEl = document.getElementById('diff-oo-vs-futures-rfc-sol'); // Removed
-                // const diffSpotFuturesEl = document.getElementById('diff-spot-vs-futures-rfc-sol'); // Removed
-
                 diffOOSpotEl.textContent = formatPercentageForTable(data.diff_oo_vs_spot_rfc_sol_percentage);
-                // diffOOFuturesEl.textContent = formatPercentageForTable(data.diff_oo_vs_futures_rfc_sol_percentage); // Removed
-                // diffSpotFuturesEl.textContent = formatPercentageForTable(data.diff_spot_vs_futures_rfc_sol_percentage); // Removed
-
-                [diffOOSpotEl/*, diffOOFuturesEl, diffSpotFuturesEl*/].forEach(el => { // Adjusted array
-                    const valStr = el.textContent.replace('%','').replace('+','');
-                    const val = parseFloat(valStr);
-                    if (!isNaN(val)) { el.className = val > 0 ? 'price-up' : (val < 0 ? 'price-down' : ''); }
-                     else { el.className = ''; }
-                });
+                const valStr = diffOOSpotEl.textContent.replace('%','').replace('+',''); const val = parseFloat(valStr);
+                if (!isNaN(val)) { diffOOSpotEl.className = val > 0 ? 'price-up' : (val < 0 ? 'price-down' : ''); } else { diffOOSpotEl.className = ''; }
                 document.getElementById('last-updated').textContent = data.last_updated || "N/A";
             } catch (error) { console.error('Error fetching table data:', error); }
         }
 
-        // --- Update Plotly Charts ---
         async function updatePlotlyCharts() {
             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();
-
-                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) {
-                        pricePlotInitialized = true;
-                        priceChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(priceChartDiv, diffChartDiv, eventData); });
+                const response = await fetch('/plotly-chart-data'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+                const chartDataResponse = await response.json();
+                const priceChartConfig = chartDataResponse.price_chart; const diffChartConfig = chartDataResponse.diff_chart;
+                const currentPriceLayout = priceChartDiv.layout; const currentDiffLayout = diffChartDiv.layout;
+
+                if (priceChartConfig && priceChartConfig.data && priceChartConfig.layout) {
+                    if (pricePlotInitialized && !isPaused && currentPriceLayout && currentPriceLayout.xaxis && !currentPriceLayout.xaxis.autorange && currentPriceLayout.xaxis.range) {
+                        priceChartConfig.layout.xaxis.range = currentPriceLayout.xaxis.range; priceChartConfig.layout.xaxis.autorange = false;
                     }
+                    Plotly.react(priceChartDiv, priceChartConfig.data, priceChartConfig.layout, {responsive: true});
+                    if (!pricePlotInitialized) { pricePlotInitialized = true; priceChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(priceChartDiv, diffChartDiv, eventData); }); }
                     priceChartStatusDiv.textContent = `价格图表更新于: ${new Date().toLocaleTimeString()}`;
                 } 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) {
-                        diffPlotInitialized = true;
-                        diffChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(diffChartDiv, priceChartDiv, eventData); });
+                if (diffChartConfig && diffChartConfig.data && diffChartConfig.layout) {
+                    if (diffPlotInitialized && !isPaused && currentDiffLayout && currentDiffLayout.xaxis && !currentDiffLayout.xaxis.autorange && currentDiffLayout.xaxis.range) {
+                        diffChartConfig.layout.xaxis.range = currentDiffLayout.xaxis.range; diffChartConfig.layout.xaxis.autorange = false;
                     }
+                    Plotly.react(diffChartDiv, diffChartConfig.data, diffChartConfig.layout, {responsive: true});
+                    if (!diffPlotInitialized) { diffPlotInitialized = true; diffChartDiv.on('plotly_relayout', (eventData) => { syncPlotlyXAxes(diffChartDiv, priceChartDiv, eventData); }); }
                     diffChartStatusDiv.textContent = `价差图表更新于: ${new Date().toLocaleTimeString()}`;
                 } else { diffChartStatusDiv.textContent = "错误: 价差图表数据无效。"; }
-
-            } catch (error) {
-                console.error('Error fetching or plotting Plotly data:', error);
-                priceChartStatusDiv.textContent = `图表更新错误: ${error.message}`;
-                diffChartStatusDiv.textContent = `图表更新错误: ${error.message}`;
-            }
+            } catch (error) { console.error('Error fetching or plotting Plotly data:', error); priceChartStatusDiv.textContent = `图表更新错误: ${error.message}`; diffChartStatusDiv.textContent = `图表更新错误: ${error.message}`; }
         }
 
-        // --- Pause/Resume ---
         function togglePauseResume() {
             isPaused = !isPaused;
-            if (isPaused) {
-                if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID); dataUpdateIntervalID = null;
-                pauseResumeButton.textContent = '继续刷新'; pauseResumeButton.classList.add('pause-button-active');
-            } else {
-                pauseResumeButton.textContent = '暂停刷新'; pauseResumeButton.classList.remove('pause-button-active');
-                updateTableData(); updatePlotlyCharts();
-                dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs);
-            }
+            if (isPaused) { if (dataUpdateIntervalID) clearInterval(dataUpdateIntervalID); dataUpdateIntervalID = null; pauseResumeButton.textContent = '继续刷新'; pauseResumeButton.classList.add('pause-button-active'); }
+            else { pauseResumeButton.textContent = '暂停刷新'; pauseResumeButton.classList.remove('pause-button-active'); updateTableData(); updatePlotlyCharts(); dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs); }
         }
         pauseResumeButton.addEventListener('click', togglePauseResume);
-
-        // --- Initial Load ---
         updateTableData(); updatePlotlyCharts();
-        if (!isPaused) {
-            dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs);
-        }
+        if (!isPaused) { dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs); }
     </script>
 </body>
 </html>