skyffire 6 months ago
parent
commit
0eaa3a5aa0
2 changed files with 216 additions and 151 deletions
  1. 150 96
      price_checker.py
  2. 66 55
      templates/index_plotly_sol_spot_only.html

+ 150 - 96
price_checker.py

@@ -12,21 +12,39 @@ import plotly.graph_objects as go
 from plotly.utils import PlotlyJSONEncoder
 
 # --- 配置部分 ---
-IN_TOKEN_ADDRESS_SOLANA = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
-OUT_TOKEN_ADDRESS_SOLANA = '0x28561B8A2360F463011c16b6Cc0B0cbEF8dbBcad'
-AMOUNT_TO_QUERY_OPENOCEAN_IN_SOL = decimal.Decimal('1')
-GATEIO_SPOT_PAIR_RFC_USDT = 'MOODENGETH_USDT'
-BINANCE_SOL_USDT_PAIR = 'ETHUSDT'
+# OpenOcean (输入 ETH, 输出目标代币, 得到 目标代币/ETH 价格)
+# Token addresses are for Ethereum Mainnet
+IN_TOKEN_ADDRESS_ETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'  # WETH Ethereum Mainnet
+OUT_TOKEN_ADDRESS_TARGET_ETH = '0xf816507E690f5Aa4E29d164885EB5fa7a5627860'  # MOODEN on Ethereum
+AMOUNT_TO_QUERY_OPENOCEAN_IN_ETH = decimal.Decimal('1')  # 例如用 1 ETH 去查询能买多少目标代币
+
+# MEXC (抹茶) 现货 (目标代币/USDT)
+# 重要: 确保这个交易对在MEXC上存在并且是 TARGET_ASSET/USDT 的形式
+MEXC_TARGET_SPOT_PAIR_USDT = 'RATO_USDT'  # 举例,你需要替换成实际的目标代币在MEXC上的交易对
+
+# Binance (ETH/USDT - 用于转换)
+BINANCE_BASE_CURRENCY_PAIR_USDT = 'ETHUSDT'  # 基础货币现在是ETH
+
 proxies = None
 decimal.getcontext().prec = 36
 
 
-# --- 价格获取函数 (与上一版相同,此处省略以保持简洁) ---
-def get_openocean_price_eth(in_token_addr, out_token_addr, human_amount_in_decimal_for_request):
-    chain = 'eth';
-    url = f'https://open-api.openocean.finance/v4/{chain}/quote'
+# --- 价格获取函数 ---
+# OpenOcean (获取 目标代币/ETH 价格)
+def get_openocean_price_vs_base_currency(chain_name, in_token_addr, out_token_addr, human_amount_in_base_currency):
+    # chain_name: 'eth', 'solana', etc.
+    # in_token_addr: 基础货币的地址 (如 WETH, Wrapped SOL)
+    # out_token_addr: 目标代币的地址
+    # human_amount_in_base_currency: 用多少基础货币去查询
+    # 返回: price_base_currency_per_target_token (即 1 目标代币 = X 基础货币)
+    url = f'https://open-api.openocean.finance/v4/{chain_name}/quote'
+    # OpenOcean ETH chain gasPrice might need adjustment. For now using a placeholder.
+    # Check OpenOcean docs for recommended gasPrice values or if it's auto-detected for ETH.
+    # Using a higher gas price for ETH as an example. You might need to fetch current gas prices.
+    gas_price_value = '50000000000' if chain_name == 'eth' else '0.000005'  # Gwei for ETH, lamports for SOL
+
     params = {'inTokenAddress': in_token_addr, 'outTokenAddress': out_token_addr,
-              'amount': str(human_amount_in_decimal_for_request), 'gasPrice': '5000000000'}
+              'amount': str(human_amount_in_base_currency), 'gasPrice': gas_price_value}
     try:
         r = requests.get(url, params=params, proxies=proxies, timeout=10);
         r.raise_for_status();
@@ -36,55 +54,58 @@ def get_openocean_price_eth(in_token_addr, out_token_addr, human_amount_in_decim
             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_sol = atomic_in / (10 ** in_dec);
-            h_out_rfc = atomic_out / (10 ** out_dec)
-            if h_out_rfc == 0: return {"error": "OO输出RFC为0"}
-            return {"price_sol_per_rfc": h_in_sol / h_out_rfc}
+            in_dec, out_dec = int(d['inToken']['decimals']), int(d['outToken']['decimals'])
+            atomic_in_base, atomic_out_target = decimal.Decimal(d['inAmount']), decimal.Decimal(d['outAmount'])
+            human_in_base = atomic_in_base / (10 ** in_dec)  # 花费的基础货币数量 (e.g., ETH)
+            human_out_target = atomic_out_target / (10 ** out_dec)  # 得到的目标代币数量
+            if human_out_target == 0: return {"error": f"OO输出目标代币为0 ({chain_name})"}
+            # 价格:1 目标代币 = X 基础货币  =>  基础货币数量 / 目标代币数量
+            price_val = human_in_base / human_out_target
+            return {f"price_base_per_target": price_val}  # e.g. price_eth_per_mooden
         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 '格式错误'}"}
+                "error": f"OO API错误({chain_name}) - 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(Solana)请求错误: {e}"}
+        return {"error": f"OO({chain_name})请求错误: {e}"}
 
 
-def get_gateio_spot_price_usdt(pair_symbol):
-    url = f'https://api.gateio.ws/api/v4/spot/tickers';
-    params = {'currency_pair': pair_symbol}
+# MEXC 现货 (获取 目标代币/USDT 价格)
+def get_mexc_spot_price_usdt(pair_symbol):
+    # MEXC API v3: https://mxcdevelop.github.io/apidocs/spot_v3_en/#ticker-price
+    # pair_symbol should be like 'BTCUSDT', 'ETHUSDT', 'MOODENUSDT'
+    url = "https://api.mexc.com/api/v3/ticker/price"
+    params = {'symbol': pair_symbol.replace('_', '')}  # MEXC uses 'BTCUSDT' not 'BTC_USDT'
     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('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})数据不匹配或无价格"}
-        elif isinstance(data, dict) and data.get('label'):
-            return {"error": f"Gate现货API错误: {data['label']}-{data.get('message', '')}"}
+        # MEXC an array of tickers if no symbol is provided, or a single object if symbol is specific
+        if isinstance(data, dict) and data.get('symbol') == params['symbol'] and 'price' in data:
+            return {"price_target_usdt": decimal.Decimal(data['price'])}
+        elif isinstance(data, list) and len(data) == 1 and data[0].get('symbol') == params['symbol'] and 'price' in \
+                data[0]:  # Sometimes it returns a list with one item
+            return {"price_target_usdt": decimal.Decimal(data[0]['price'])}
         else:
-            return {"error": f"Gate现货API({pair_symbol})数据格式错误或未找到"}
+            error_msg = data.get('msg', '未知MEXC错误或交易对不存在') if isinstance(data, dict) else 'MEXC响应格式不正确'
+            return {"error": f"MEXC现货({pair_symbol}) API错误: {error_msg}"}
     except Exception as e:
-        return {"error": f"Gate现货({pair_symbol})请求错误: {e}"}
+        return {"error": f"MEXC现货({pair_symbol})请求错误: {e}"}
 
 
-def get_binance_spot_price_usdt(symbol):
+# Binance (获取 基础货币/USDT 价格, e.g., ETH/USDT)
+def get_binance_base_currency_price_usdt(symbol):
     url = "https://api.binance.com/api/v3/ticker/price";
-    params = {'symbol': symbol}
+    params = {'symbol': symbol.replace('_', '')}
     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'])}
+        if 'price' in data and data.get('symbol') == params['symbol']:
+            return {"price_base_usdt": decimal.Decimal(data['price'])}  # e.g. price_eth_usdt
         else:
             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"Binance API ({symbol}) 意外错误: {e}"}
+        return {"error": f"Binance API ({symbol}) 错误: {e}"}
 
 
 app = Flask(__name__)
@@ -95,12 +116,21 @@ MAX_HISTORY_POINTS_PLOTLY = 86400
 REFRESH_INTERVAL_SECONDS = 1
 
 historical_data_points = deque(maxlen=MAX_HISTORY_POINTS_PLOTLY)
+
+# Extract target asset and base currency symbols from config
+TARGET_ASSET_SYMBOL = MEXC_TARGET_SPOT_PAIR_USDT.split('_')[0]  # e.g., MOODEN
+BASE_CURRENCY_SYMBOL = BINANCE_BASE_CURRENCY_PAIR_USDT.replace('USDT', '')  # e.g., ETH
+
 latest_values_for_table = {
-    "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",
-    "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]
+    f"oo_target_vs_base_price": "N/A",  # e.g., oo_mooden_vs_eth_price
+    f"mexc_target_vs_base_price": "N/A",  # e.g., mexc_mooden_vs_eth_price (converted)
+    f"binance_base_vs_usdt_price": "N/A",  # e.g., binance_eth_vs_usdt_price (reference)
+    f"diff_oo_vs_mexc_target_base_percentage": "N/A",
+    "oo_error": None, "mexc_error": None, "binance_error": None,
+    "last_updated": "N/A",
+    "mexc_spot_pair_usdt": MEXC_TARGET_SPOT_PAIR_USDT,  # Original MEXC pair
+    "target_asset_symbol_for_display": TARGET_ASSET_SYMBOL,
+    "base_currency_symbol_for_display": BASE_CURRENCY_SYMBOL
 }
 data_lock = threading.Lock()
 
@@ -112,58 +142,78 @@ def calculate_percentage_diff(price_a, price_b):
     return None
 
 
-def convert_usdt_price_to_sol_price(price_asset_usdt, 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
+def convert_usdt_price_to_base_currency_price(price_target_usdt, price_base_usdt):
+    # Converts Price(Target/USDT) to Price(Target/BaseCurrency) using Price(BaseCurrency/USDT)
+    # e.g., MOODEN/ETH = MOODEN/USDT / ETH/USDT
+    if price_target_usdt is not None and price_base_usdt is not None and price_base_usdt > 0:
+        return price_target_usdt / price_base_usdt
     return None
 
 
 def update_data_for_plotly_and_table():
     global historical_data_points, latest_values_for_table
-    print("数据更新线程启动 (仅现货)...")
+    print(f"数据更新线程启动 ({TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL})...")
     while True:
         fetch_time_full = time.strftime("%Y-%m-%d %H:%M:%S");
         fetch_time_chart = time.strftime("%H:%M:%S")
-        oo_data = get_openocean_price_eth(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)
-        binance_sol_usdt_data = get_binance_spot_price_usdt(BINANCE_SOL_USDT_PAIR)
-        oo_rfc_sol_price = oo_data.get("price_sol_per_rfc");
-        spot_rfc_usdt_price = spot_usdt_data.get("price_rfc_usdt")
-        sol_usdt_price = binance_sol_usdt_data.get("price_sol_usdt")
+
+        # OpenOcean: TARGET_ASSET / BASE_CURRENCY (e.g., MOODEN/ETH)
+        oo_data = get_openocean_price_vs_base_currency('eth', IN_TOKEN_ADDRESS_ETH, OUT_TOKEN_ADDRESS_TARGET_ETH,
+                                                       AMOUNT_TO_QUERY_OPENOCEAN_IN_ETH)
+
+        # MEXC: TARGET_ASSET / USDT (e.g., MOODEN/USDT)
+        mexc_target_usdt_data = get_mexc_spot_price_usdt(MEXC_TARGET_SPOT_PAIR_USDT)
+
+        # Binance: BASE_CURRENCY / USDT (e.g., ETH/USDT)
+        binance_base_usdt_data = get_binance_base_currency_price_usdt(BINANCE_BASE_CURRENCY_PAIR_USDT)
+
+        oo_target_vs_base_price = oo_data.get("price_base_per_target")  # 1 TARGET = X BASE
+        mexc_target_usdt_price = mexc_target_usdt_data.get("price_target_usdt")  # 1 TARGET = X USDT
+        base_usdt_price = binance_base_usdt_data.get("price_base_usdt")  # 1 BASE = X USDT
+
         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)
-        diff_oo_spot_pct = calculate_percentage_diff(oo_rfc_sol_price, spot_rfc_sol_price)
+        mexc_err = mexc_target_usdt_data.get("error");
+        binance_err = binance_base_usdt_data.get("error")
+
+        # Convert MEXC price to TARGET_ASSET/BASE_CURRENCY
+        mexc_target_vs_base_price = convert_usdt_price_to_base_currency_price(mexc_target_usdt_price, base_usdt_price)
+
+        diff_oo_vs_mexc_pct = calculate_percentage_diff(oo_target_vs_base_price, mexc_target_vs_base_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,
-            "diff_oo_spot_rfc_sol": float(diff_oo_spot_pct) if diff_oo_spot_pct is not None else None,
+            "oo_target_vs_base": float(oo_target_vs_base_price) if oo_target_vs_base_price else None,
+            "mexc_target_vs_base": float(mexc_target_vs_base_price) if mexc_target_vs_base_price else None,
+            "diff_oo_vs_mexc_target_base": float(diff_oo_vs_mexc_pct) if diff_oo_vs_mexc_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["sol_usdt_price_binance"] = str(sol_usdt_price) if sol_usdt_price else "N/A"
+            latest_values_for_table[f"oo_target_vs_base_price"] = str(
+                oo_target_vs_base_price) if oo_target_vs_base_price else "N/A"
+            latest_values_for_table[f"mexc_target_vs_base_price"] = str(
+                mexc_target_vs_base_price) if mexc_target_vs_base_price else "N/A"
+            latest_values_for_table[f"binance_base_vs_usdt_price"] = str(base_usdt_price) if base_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"
+                f"diff_oo_vs_mexc_target_base_percentage"] = f"{diff_oo_vs_mexc_pct:+.4f}%" if diff_oo_vs_mexc_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["binance_sol_error"] = binance_err;
+            latest_values_for_table["mexc_error"] = mexc_err
+            latest_values_for_table["binance_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'}")
+            f"{fetch_time_chart} Fetch | OO_{TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL}:{'OK' if oo_target_vs_base_price else 'F'} | MEXC_{TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL}:{'OK' if mexc_target_vs_base_price else 'F'} | {BASE_CURRENCY_SYMBOL}/USDT:{'OK' if base_usdt_price else 'F'}")
         time.sleep(REFRESH_INTERVAL_SECONDS)
 
 
 @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_dynamic.html',
+                           target_asset=TARGET_ASSET_SYMBOL,
+                           base_asset=BASE_CURRENCY_SYMBOL,
+                           mexc_spot_pair_usdt=MEXC_TARGET_SPOT_PAIR_USDT,
+                           binance_bridge_pair=BINANCE_BASE_CURRENCY_PAIR_USDT,
                            refresh_interval_ms=REFRESH_INTERVAL_SECONDS * 1000)
 
 
@@ -183,7 +233,11 @@ def get_plotly_chart_data():
             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"]
+
+        # 使用从配置中提取的动态符号
+        display_target_asset = latest_values_for_table["target_asset_symbol_for_display"]
+        display_base_asset = latest_values_for_table["base_currency_symbol_for_display"]
+        display_mexc_pair_usdt = latest_values_for_table["mexc_spot_pair_usdt"]
 
         common_xaxis_config = dict(title='时间')
         if len(times) > 1:
@@ -194,29 +248,29 @@ def get_plotly_chart_data():
         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)',
-                       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)',
+        fig_prices.add_trace(go.Scatter(x=times, y=[p['oo_target_vs_base'] for p in points], mode='lines',
+                                        name=f'OpenOcean ({display_target_asset}/{display_base_asset})',
+                                        line=dict(color='rgb(75, 192, 192)'),
+                                        hovertemplate=f'<b>OpenOcean ({display_target_asset}/{display_base_asset})</b><br>Time: %{{x}}<br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>'))
+        fig_prices.add_trace(go.Scatter(x=times, y=[p['mexc_target_vs_base'] for p in points], mode='lines',
+                                        name=f'MEXC ({display_mexc_pair_usdt} → {display_target_asset}/{display_base_asset})',
                                         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.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='平台',
+                                        hovertemplate=f'<b>MEXC ({display_target_asset}/{display_base_asset})</b><br>Time: %{{x}}<br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>'))
+        fig_prices.update_layout(title_text=f'{display_target_asset}/{display_base_asset} 价格历史',
+                                 xaxis=common_xaxis_config.copy(),
+                                 yaxis_title=f'价格 (1 {display_target_asset} = X {display_base_asset})',
+                                 legend_title_text='平台',
                                  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.update_layout(title_text=f'{target_asset_symbol}/SOL 价差百分比历史 (OO vs Spot)',
+        fig_diffs.add_trace(go.Scatter(x=times, y=[p['diff_oo_vs_mexc_target_base'] for p in points], mode='lines',
+                                       name=f'OO vs MEXC ({display_target_asset}/{display_base_asset})',
+                                       line=dict(color='rgb(255, 159, 64)'),
+                                       hovertemplate=f'<b>OO vs MEXC ({display_target_asset}/{display_base_asset})</b><br>Time: %{{x}}<br>Diff: %{{y:+.4f}}%<extra></extra>'))
+        fig_diffs.update_layout(title_text=f'{display_target_asset}/{display_base_asset} 价差百分比历史 (OO vs MEXC)',
                                 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_title='价差 (%)', legend_title_text='对比', legend=common_legend_config.copy(),
                                 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)),
@@ -226,13 +280,13 @@ def get_plotly_chart_data():
 
 if __name__ == "__main__":
     print("应用启动...")
-    asset_symbol = GATEIO_SPOT_PAIR_RFC_USDT.split('_')[0]
-    print(f"目标资产: {asset_symbol}")
+    print(f"目标资产: {TARGET_ASSET_SYMBOL}")
+    print(f"基础货币 (用于计价): {BASE_CURRENCY_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"币安转换汇率: {BINANCE_SOL_USDT_PAIR}")
+        f"OpenOcean: {TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL} (通过 {IN_TOKEN_ADDRESS_ETH[-6:]}...{BASE_CURRENCY_SYMBOL} / {OUT_TOKEN_ADDRESS_TARGET_ETH[-6:]}...{TARGET_ASSET_SYMBOL} on ETH)")
+    print(f"MEXC 现货: {MEXC_TARGET_SPOT_PAIR_USDT} (将转换为 {TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL})")
+    print(f"币安转换汇率: {BINANCE_BASE_CURRENCY_PAIR_USDT}")
     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)
+    app.run(debug=False, host='0.0.0.0', port=5000, use_reloader=False)

+ 66 - 55
templates/index_plotly_sol_spot_only.html

@@ -3,63 +3,76 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>{{ target_asset }}/SOL 价格监控 (Plotly - 现货Only)</title>
+    <title>{{ target_asset }}/{{ base_asset }} 价格监控 (Plotly)</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; }
         table { width: 100%; border-collapse: collapse; margin-top: 10px; margin-bottom: 15px; }
         th, td { border: 1px solid #ddd; padding: 8px; text-align: left; font-size:0.85em; word-break: break-all;}
         th { background-color: #e9e9e9; white-space: nowrap; }
-        .price-up { color: green; }
-        .price-down { color: red; }
-        .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: 400px; margin: 20px auto; } /* 主图默认高度 */
-        .diff-chart-container { height: 280px; } /* 副图特定高度 */
+        .price-up { color: green; } .price-down { color: red; } .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: 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; }
-        .pause-button-active { background-color: #ffc107; color: #333; }
-        .platform-name {font-weight: bold;}
-        #main-title { font-size: 1.8em; }
-        h2 { font-size: 1.3em; }
+        .control-button:hover { background-color: #0056b3; } .pause-button-active { background-color: #ffc107; color: #333; }
+        .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-size:0.8em; font-style: italic; color: grey; }
     </style>
 </head>
 <body>
     <div class="container">
-        <h1 id="main-title">{{ target_asset }}/SOL 价格监控 (现货 vs OO)</h1>
+        <!-- 使用 target_asset 和 base_asset 动态生成标题 -->
+        <h1 id="main-title">{{ target_asset }}/{{ base_asset }} 多平台价格监控</h1>
         <div class="controls-container">
             <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>价格 ({{ base_asset }})</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>
-                <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 }}/{{ base_asset }})</td>
+                    <td id="oo-target-vs-base-price">加载中...</td> <!-- 动态ID -->
+                    <td id="oo-status" class="status-cell"></td>
+                </tr>
+                <tr>
+                    <td class="platform-name" id="mexc-spot-label">MEXC 现货 ({{ mexc_spot_pair_usdt }} → {{ target_asset }}/{{ base_asset }})</td>
+                    <td id="mexc-target-vs-base-price">加载中...</td> <!-- 动态ID -->
+                    <td id="mexc-status" class="status-cell"></td>
+                </tr>
+                <tr>
+                    <td class="platform-name">参考汇率 (Binance)</td>
+                    <td id="binance-base-vs-usdt-price" title="{{ binance_bridge_pair }}">加载中... (USDT)</td> <!-- 动态ID -->
+                    <td id="binance-status" class="status-cell"></td>
+                </tr>
             </tbody>
         </table>
-        <h2>价差百分比 (基于 {{ target_asset }}/SOL 价格)</h2>
+        <h2>价差百分比 (基于 {{ target_asset }}/{{ base_asset }} 价格)</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>
+            <tbody>
+                <tr>
+                    <td id="diff-label-oo-mexc">OO vs MEXC 现货 (转换后)</td>
+                    <td id="diff-oo-vs-mexc-target-base">计算中...</td> <!-- 动态ID -->
+                </tr>
+            </tbody>
         </table>
         <div class="timestamp">最后更新: <span id="last-updated">N/A</span></div>
     </div>
 
     <div class="container">
-        <h2 id="price-chart-title">{{ target_asset }}/SOL 价格历史曲线</h2>
+        <h2 id="price-chart-title">{{ target_asset }}/{{ base_asset }} 价格历史曲线</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">{{ target_asset }}/SOL 价差百分比历史曲线 (OO vs Spot)</h2>
-        <div id='diffPercentageChartPlotly' class="chart-container diff-chart-container"></div> <!-- Applied diff-chart-container class -->
+        <h2 id="diff-chart-title">{{ target_asset }}/{{ base_asset }} 价差百分比历史曲线 (OO vs MEXC)</h2>
+        <div id='diffPercentageChartPlotly' class="chart-container diff-chart-container"></div>
         <div id="diff-chart-status" class="status-line">加载价差图表...</div>
     </div>
 
@@ -71,33 +84,30 @@
         const pauseResumeButton = document.getElementById('pause-resume-button');
 
         const refreshIntervalMs = {{ refresh_interval_ms }};
-        let dataUpdateIntervalID = null;
-        let isPaused = false;
-        let pricePlotInitialized = false;
-        let diffPlotInitialized = false;
-        let isSyncingLayout = false;
+        let dataUpdateIntervalID = null, isPaused = false, pricePlotInitialized = false, diffPlotInitialized = false, isSyncingLayout = false;
+
+        // 从Flask获取的动态符号,用于JS内部如果需要 (虽然主要在后端处理了)
+        const TARGET_ASSET = "{{ target_asset }}";
+        const BASE_ASSET = "{{ base_asset }}";
 
-        function formatPriceForTable(priceStr, precision = 8) {
+        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";
             if (price === 0) return (0).toFixed(precision);
             if (Math.abs(price) < 1e-9 && price !== 0) return price.toExponential(3);
             return price.toFixed(precision);
         }
-        function formatPercentageForTable(percStr) {
+        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";
             return `${perc > 0 ? '+' : ''}${perc.toFixed(4)}%`;
         }
-
-        function syncPlotlyXAxes(sourceDiv, targetDiv, eventData) {
-            if (isSyncingLayout) return; isSyncingLayout = true;
-            const update = {}; let newXRange = null;
+        function syncPlotlyXAxes(sourceDiv, targetDiv, eventData) { /* ...与之前相同... */
+            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;
+                newXRange = [eventData['xaxis.range[0]'], eventData['xaxis.range[1]']]; update['xaxis.range'] = newXRange; update['xaxis.autorange'] = false;
             } else {
                 if (sourceDiv.layout && sourceDiv.layout.xaxis) {
                     if (sourceDiv.layout.xaxis.autorange) { update['xaxis.autorange'] = true; }
@@ -113,52 +123,53 @@
             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-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('sol-usdt-price-binance').textContent = formatPriceForTable(data.sol_usdt_price_binance, 4);
+                // 使用JS变量动态生成ID或直接使用硬编码的ID(如果ID在HTML中已固定好)
+                document.getElementById('oo-target-vs-base-price').textContent = formatPriceForTable(data.oo_target_vs_base_price);
+                document.getElementById('mexc-target-vs-base-price').textContent = formatPriceForTable(data.mexc_target_vs_base_price);
+                document.getElementById('binance-base-vs-usdt-price').textContent = formatPriceForTable(data.binance_base_vs_usdt_price, 4);
+
                 document.getElementById('oo-status').textContent = data.oo_error || '正常';
-                document.getElementById('gate-spot-status').textContent = data.gate_spot_error || '正常';
-                document.getElementById('binance-sol-status').textContent = data.binance_sol_error || '正常';
+                document.getElementById('mexc-status').textContent = data.mexc_error || '正常';
+                document.getElementById('binance-status').textContent = data.binance_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('binance-sol-status').className = data.binance_sol_error ? 'status-cell error-message' : 'status-cell';
-                const diffOOSpotEl = document.getElementById('diff-oo-vs-spot-rfc-sol');
-                diffOOSpotEl.textContent = formatPercentageForTable(data.diff_oo_vs_spot_rfc_sol_percentage);
-                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('mexc-status').className = data.mexc_error ? 'status-cell error-message' : 'status-cell';
+                document.getElementById('binance-status').className = data.binance_error ? 'status-cell error-message' : 'status-cell';
+
+                const diffOOMexcEl = document.getElementById('diff-oo-vs-mexc-target-base');
+                diffOOMexcEl.textContent = formatPercentageForTable(data.diff_oo_vs_mexc_target_base_percentage);
+                const valStr = diffOOMexcEl.textContent.replace('%','').replace('+',''); const val = parseFloat(valStr);
+                if (!isNaN(val)) { diffOOMexcEl.className = val > 0 ? 'price-up' : (val < 0 ? 'price-down' : ''); } else { diffOOMexcEl.className = ''; }
                 document.getElementById('last-updated').textContent = data.last_updated || "N/A";
             } catch (error) { console.error('Error fetching table data:', error); }
         }
 
-        async function updatePlotlyCharts() {
+        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 chartDataResponse = await response.json();
-                const priceChartConfig = chartDataResponse.price_chart; const diffChartConfig = chartDataResponse.diff_chart;
+                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()}`;
+                    priceChartStatusDiv.textContent = `价格图表 (${TARGET_ASSET}/${BASE_ASSET}) 更新于: ${new Date().toLocaleTimeString()}`;
                 } else { priceChartStatusDiv.textContent = "错误: 价格图表数据无效。"; }
-
                 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()}`;
+                    diffChartStatusDiv.textContent = `价差图表 (${TARGET_ASSET}/${BASE_ASSET}) 更新于: ${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}`; }
         }
 
-        function togglePauseResume() {
+        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); }