Browse Source

mexc contract v2 bid ask

skyffire 5 months ago
parent
commit
480e752cb5
3 changed files with 212 additions and 304 deletions
  1. 112 122
      price_checker.py
  2. 100 0
      templates/index_plotly_dynamic.html
  3. 0 182
      templates/index_plotly_sol_spot_only.html

+ 112 - 122
price_checker.py

@@ -11,38 +11,20 @@ import plotly
 import plotly.graph_objects as go
 from plotly.utils import PlotlyJSONEncoder
 
-# --- 配置部分 ---
-# 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
-
+# --- 配置部分 (与你提供的一致) ---
+IN_TOKEN_ADDRESS_ETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
+OUT_TOKEN_ADDRESS_TARGET_ETH = '0xf816507E690f5Aa4E29d164885EB5fa7a5627860'
+AMOUNT_TO_QUERY_OPENOCEAN_IN_ETH = decimal.Decimal('1')
+MEXC_TARGET_CONTRACT_PAIR_USDT = 'RATO_USDT'  # 重命名以明确是合约
+BINANCE_BASE_CURRENCY_PAIR_USDT = 'ETHUSDT'
 proxies = None
 decimal.getcontext().prec = 36
 
 
 # --- 价格获取函数 ---
-# 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
-
+    gas_price_value = '50000000000' if chain_name == 'eth' else '0.000005'
     params = {'inTokenAddress': in_token_addr, 'outTokenAddress': out_token_addr,
               'amount': str(human_amount_in_base_currency), 'gasPrice': gas_price_value}
     try:
@@ -54,14 +36,12 @@ def get_openocean_price_vs_base_currency(chain_name, in_token_addr, out_token_ad
             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_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)  # 得到的目标代币数量
+            human_in_base = atomic_in_base / (10 ** in_dec);
+            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
+            return {"price_base_per_target": human_in_base / human_out_target}
         else:
             return {
                 "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 '格式错误'}"}
@@ -69,29 +49,31 @@ def get_openocean_price_vs_base_currency(chain_name, in_token_addr, out_token_ad
         return {"error": f"OO({chain_name})请求错误: {e}"}
 
 
-# MEXC 获取价格
-def get_mexc_contract_price_usdt(pair_symbol):
+# MEXC 合约,同时获取 ask1 和 bid1
+def get_mexc_contract_ticker_usdt(pair_symbol):  # Changed name for clarity
     url = "https://contract.mexc.com/api/v1/contract/ticker"
+    # MEXC合约交易对通常是 BASE_QUOTE,例如 RATO_USDT
     params = {'symbol': pair_symbol}
     try:
         r = requests.get(url, params=params, proxies=proxies, timeout=10);
         r.raise_for_status();
-        data = r.json()
-        data = data['data']
-        # 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 'ask1' in data:
-            return {"price_target_usdt": decimal.Decimal(data['ask1'])}
-        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'])}
+        response_data = r.json()
+        if response_data.get('success') and response_data.get('data'):
+            data = response_data['data']
+            if data.get('symbol') == params['symbol'] and 'ask1' in data and 'bid1' in data:
+                return {
+                    "price_target_usdt_ask1": decimal.Decimal(data['ask1']),
+                    "price_target_usdt_bid1": decimal.Decimal(data['bid1'])
+                }
+            else:
+                return {"error": f"MEXC合约({pair_symbol}) API响应中缺少ask1或bid1"}
         else:
-            error_msg = data.get('msg', '未知MEXC错误或交易对不存在') if isinstance(data, dict) else 'MEXC响应格式不正确'
+            error_msg = response_data.get('message', '未知MEXC错误或合约不存在')
             return {"error": f"MEXC合约({pair_symbol}) API错误: {error_msg}"}
     except Exception as e:
         return {"error": f"MEXC合约({pair_symbol})请求错误: {e}"}
 
 
-# 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.replace('_', '')}
@@ -100,7 +82,7 @@ def get_binance_base_currency_price_usdt(symbol):
         response.raise_for_status();
         data = response.json()
         if 'price' in data and data.get('symbol') == params['symbol']:
-            return {"price_base_usdt": decimal.Decimal(data['price'])}  # e.g. price_eth_usdt
+            return {"price_base_usdt": decimal.Decimal(data['price'])}
         else:
             msg = data.get('msg', '未知错误'); return {"error": f"Binance API错误 ({symbol}): {msg}"}
     except Exception as e:
@@ -110,151 +92,152 @@ def get_binance_base_currency_price_usdt(symbol):
 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)
 
-# 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
+TARGET_ASSET_SYMBOL = MEXC_TARGET_CONTRACT_PAIR_USDT.split('_')[0]
+BASE_CURRENCY_SYMBOL = BINANCE_BASE_CURRENCY_PAIR_USDT.replace('USDT', '')
 
 latest_values_for_table = {
-    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",
+    f"oo_target_vs_base_price": "N/A",
+    f"mexc_target_vs_base_price_ask1": "N/A",  # MEXC Ask1
+    f"mexc_target_vs_base_price_bid1": "N/A",  # MEXC Bid1
+    f"binance_base_vs_usdt_price": "N/A",
+    f"diff_oo_vs_mexc_ask1_percentage": "N/A",  # Diff OO vs MEXC Ask1
+    f"diff_oo_vs_mexc_bid1_percentage": "N/A",  # Diff OO vs MEXC Bid1
     "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
+    "mexc_contract_pair_usdt": MEXC_TARGET_CONTRACT_PAIR_USDT,
     "target_asset_symbol_for_display": TARGET_ASSET_SYMBOL,
     "base_currency_symbol_for_display": BASE_CURRENCY_SYMBOL
 }
 data_lock = threading.Lock()
 
 
-def calculate_percentage_diff(price_a, price_b):
+def calculate_percentage_diff(price_a, price_b):  # No change
     if price_a is not None and price_b is not None and isinstance(price_a, decimal.Decimal) and isinstance(price_b,
-                                                                                                           decimal.Decimal) and price_b != 0:
-        return ((price_a - price_b) / price_b) * 100
+                                                                                                           decimal.Decimal) and price_b != 0: return (
+                (price_a - price_b) / price_b) * 100
     return None
 
 
-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
+def convert_usdt_price_to_base_currency_price(price_target_usdt, price_base_usdt):  # No change
+    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(f"数据更新线程启动 ({TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL})...")
+    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")
-
-        # 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_contract_price_usdt(MEXC_TARGET_SPOT_PAIR_USDT)
-
-        # Binance: BASE_CURRENCY / USDT (e.g., ETH/USDT)
+        mexc_ticker_usdt_data = get_mexc_contract_ticker_usdt(MEXC_TARGET_CONTRACT_PAIR_USDT)  # Get full ticker
         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_target_vs_base_price = oo_data.get("price_base_per_target")
+        mexc_target_usdt_price_ask1 = mexc_ticker_usdt_data.get("price_target_usdt_ask1")
+        mexc_target_usdt_price_bid1 = mexc_ticker_usdt_data.get("price_target_usdt_bid1")
+        base_usdt_price = binance_base_usdt_data.get("price_base_usdt")
 
         oo_err = oo_data.get("error");
-        mexc_err = mexc_target_usdt_data.get("error");
+        mexc_err = mexc_ticker_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)
+        mexc_target_vs_base_price_ask1 = convert_usdt_price_to_base_currency_price(mexc_target_usdt_price_ask1,
+                                                                                   base_usdt_price)
+        mexc_target_vs_base_price_bid1 = convert_usdt_price_to_base_currency_price(mexc_target_usdt_price_bid1,
+                                                                                   base_usdt_price)
 
-        diff_oo_vs_mexc_pct = calculate_percentage_diff(oo_target_vs_base_price, mexc_target_vs_base_price)
+        diff_oo_vs_mexc_ask1_pct = calculate_percentage_diff(oo_target_vs_base_price, mexc_target_vs_base_price_ask1)
+        diff_oo_vs_mexc_bid1_pct = calculate_percentage_diff(oo_target_vs_base_price, mexc_target_vs_base_price_bid1)
 
         current_point = {
             "time": fetch_time_chart,
             "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,
+            "mexc_target_vs_base_ask1": float(
+                mexc_target_vs_base_price_ask1) if mexc_target_vs_base_price_ask1 else None,
+            "mexc_target_vs_base_bid1": float(
+                mexc_target_vs_base_price_bid1) if mexc_target_vs_base_price_bid1 else None,
+            "diff_oo_vs_mexc_ask1": float(diff_oo_vs_mexc_ask1_pct) if diff_oo_vs_mexc_ask1_pct is not None else None,
+            "diff_oo_vs_mexc_bid1": float(diff_oo_vs_mexc_bid1_pct) if diff_oo_vs_mexc_bid1_pct is not None else None,
         }
-
         with data_lock:
             historical_data_points.append(current_point)
             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"mexc_target_vs_base_price_ask1"] = str(
+                mexc_target_vs_base_price_ask1) if mexc_target_vs_base_price_ask1 else "N/A"
+            latest_values_for_table[f"mexc_target_vs_base_price_bid1"] = str(
+                mexc_target_vs_base_price_bid1) if mexc_target_vs_base_price_bid1 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[
-                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"
+                f"diff_oo_vs_mexc_ask1_percentage"] = f"{diff_oo_vs_mexc_ask1_pct:+.4f}%" if diff_oo_vs_mexc_ask1_pct is not None else "N/A"
+            latest_values_for_table[
+                f"diff_oo_vs_mexc_bid1_percentage"] = f"{diff_oo_vs_mexc_bid1_pct:+.4f}%" if diff_oo_vs_mexc_bid1_pct is not None else "N/A"
             latest_values_for_table["oo_error"] = oo_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_{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'}")
+        ok_oo = 'OK' if oo_target_vs_base_price else 'F';
+        ok_mexc_ask = 'OK' if mexc_target_vs_base_price_ask1 else 'F'
+        ok_mexc_bid = 'OK' if mexc_target_vs_base_price_bid1 else 'F';
+        ok_base = 'OK' if base_usdt_price else 'F'
+        print(f"{fetch_time_chart} Fetch | OO:{ok_oo} | MEXC Ask1:{ok_mexc_ask} Bid1:{ok_mexc_bid} | Base:{ok_base}")
         time.sleep(REFRESH_INTERVAL_SECONDS)
 
 
 @app.route('/')
 def index_plotly():
-    # 这些符号将传递给模板用于动态显示
-    return render_template('index_plotly_dynamic.html',
-                           target_asset=TARGET_ASSET_SYMBOL,
+    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,
+                           mexc_contract_pair_usdt=MEXC_TARGET_CONTRACT_PAIR_USDT,  # Changed from spot to contract
                            binance_bridge_pair=BINANCE_BASE_CURRENCY_PAIR_USDT,
                            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')
 def get_plotly_chart_data():
     with data_lock:
         points = list(historical_data_points)
-        if not points:
-            fig = go.Figure();
-            fig.update_layout(title_text="暂无数据")
-            empty_json = json.loads(json.dumps(fig, cls=PlotlyJSONEncoder))
-            return jsonify({"price_chart": empty_json, "diff_chart": empty_json})
-
+        if not points: fig = go.Figure(); fig.update_layout(title_text="暂无数据"); 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]
-
-        # 使用从配置中提取的动态符号
         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"]
+        display_mexc_pair_usdt = latest_values_for_table["mexc_contract_pair_usdt"]  # Changed
 
-        common_xaxis_config = dict(title='时间')
+        common_xaxis_config = dict(title='时间');
+        common_legend_config = dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
         if len(times) > 1:
-            common_xaxis_config['range'] = [times[0], 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_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})',
+                                        hovertemplate=f'<b>OO</b><br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>'))
+        fig_prices.add_trace(go.Scatter(x=times, y=[p['mexc_target_vs_base_ask1'] for p in points], mode='lines',
+                                        name=f'MEXC Ask1 ({display_target_asset}/{display_base_asset})',
                                         line=dict(color='rgb(255, 99, 132)'),
-                                        hovertemplate=f'<b>MEXC ({display_target_asset}/{display_base_asset})</b><br>Time: %{{x}}<br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>'))
+                                        hovertemplate=f'<b>MEXC Ask1</b><br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>'))
+        # Add MEXC Bid1 trace
+        fig_prices.add_trace(go.Scatter(x=times, y=[p['mexc_target_vs_base_bid1'] for p in points], mode='lines',
+                                        name=f'MEXC Bid1 ({display_target_asset}/{display_base_asset})',
+                                        line=dict(color='rgb(255, 0, 0)', dash='dash'),
+                                        hovertemplate=f'<b>MEXC Bid1</b><br>Price: %{{y:.8f}} {display_base_asset}<extra></extra>'))  # Different color/style for Bid1
+
         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})',
@@ -262,30 +245,37 @@ def get_plotly_chart_data():
                                  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_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)',
+        fig_diffs = go.Figure()  # Diff chart may need adjustment if you want to show diffs against bid1 too
+        fig_diffs.add_trace(
+            go.Scatter(x=times, y=[p['diff_oo_vs_mexc_ask1'] for p in points], mode='lines', name=f'OO vs MEXC Ask1',
+                       line=dict(color='rgb(255, 159, 64)'),
+                       hovertemplate=f'<b>OO vs MEXC Ask1</b><br>Diff: %{{y:+.4f}}%<extra></extra>'))
+        # Optionally, add diff for OO vs MEXC Bid1
+        fig_diffs.add_trace(
+            go.Scatter(x=times, y=[p['diff_oo_vs_mexc_bid1'] for p in points], mode='lines', name=f'OO vs MEXC Bid1',
+                       line=dict(color='rgb(200, 100, 50)', dash='dot'),
+                       hovertemplate=f'<b>OO vs MEXC Bid1</b><br>Diff: %{{y:+.4f}}%<extra></extra>'))
+
+        fig_diffs.update_layout(title_text=f'{display_target_asset}/{display_base_asset} 价差百分比历史',
                                 xaxis=common_xaxis_config.copy(),
                                 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)),
-                                "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder))}
+                                "diff_chart": json.loads(json.dumps(fig_diffs, cls=PlotlyJSONEncoder))};
         return jsonify(combined_figure_data)
 
 
-if __name__ == "__main__":
-    print("应用启动...")
-    print(f"目标资产: {TARGET_ASSET_SYMBOL}")
+if __name__ == "__main__":  # No change in __main__ needed for this specific request
+    print("应用启动...");
+    print(f"目标资产: {TARGET_ASSET_SYMBOL}");
     print(f"基础货币 (用于计价): {BASE_CURRENCY_SYMBOL}")
     print(
         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"MEXC 合约: {MEXC_TARGET_CONTRACT_PAIR_USDT} (将转换为 {TARGET_ASSET_SYMBOL}/{BASE_CURRENCY_SYMBOL})")  # Changed spot to contract
     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)")
+    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)

File diff suppressed because it is too large
+ 100 - 0
templates/index_plotly_dynamic.html


+ 0 - 182
templates/index_plotly_sol_spot_only.html

@@ -1,182 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <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; }
-        .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; }
-        .status-line { text-align: center; margin-top: 10px; font-size:0.8em; font-style: italic; color: grey; }
-    </style>
-</head>
-<body>
-    <div class="container">
-        <!-- 使用 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>价格 ({{ base_asset }})</th><th class="status-cell">状态/错误</th></tr> </thead>
-            <tbody>
-                <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 }}/{{ base_asset }} 价格)</h2>
-        <table>
-             <thead><tr><th>对比</th><th>价差 (%)</th></tr></thead>
-            <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 }}/{{ 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 }}/{{ 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>
-
-    <script type='text/javascript'>
-        const priceChartDiv = document.getElementById('priceHistoryChartPlotly');
-        const diffChartDiv = document.getElementById('diffPercentageChartPlotly');
-        const priceChartStatusDiv = document.getElementById('price-chart-status');
-        const diffChartStatusDiv = document.getElementById('diff-chart-status');
-        const pauseResumeButton = document.getElementById('pause-resume-button');
-
-        const refreshIntervalMs = {{ refresh_interval_ms }};
-        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) { /* ...与之前相同... */
-            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) { /* ...与之前相同... */
-            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;
-            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) {
-                    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; }
-            }
-            Plotly.relayout(targetDiv, update).then(() => { isSyncingLayout = false; }).catch(e => { console.error("Error syncing layout:", e); isSyncingLayout = false; });
-        }
-
-        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 data = await response.json();
-                // 使用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('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('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() { /* ...与之前相同... */
-            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 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 = `价格图表 (${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 = `价差图表 (${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() { /* ...与之前相同... */
-            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); }
-        }
-        pauseResumeButton.addEventListener('click', togglePauseResume);
-        updateTableData(); updatePlotlyCharts();
-        if (!isPaused) { dataUpdateIntervalID = setInterval(() => { updateTableData(); updatePlotlyCharts(); }, refreshIntervalMs); }
-    </script>
-</body>
-</html>

Some files were not shown because too many files changed in this diff