|
|
@@ -1,108 +1,75 @@
|
|
|
import requests
|
|
|
-import decimal # 导入 decimal 模块,用于更精确的货币运算
|
|
|
-import time # 导入 time 模块,用于实现轮询间隔
|
|
|
-
|
|
|
-# --- 配置部分 (与之前代码相同,此处省略以保持简洁) ---
|
|
|
-# proxies = None # 如果不使用代理
|
|
|
+import decimal
|
|
|
+import time
|
|
|
+import threading
|
|
|
+import json
|
|
|
+from flask import Flask, render_template, jsonify
|
|
|
+from collections import deque # 用于高效地存储固定数量的历史数据
|
|
|
+
|
|
|
+# --- 配置部分 (与之前相同) ---
|
|
|
GATEIO_SPOT_PAIR = 'MUBARAK_USDT'
|
|
|
-# BSC (币安智能链) 代币地址
|
|
|
-IN_TOKEN_ADDRESS_BSC = '0x55d398326f99059ff775485246999027b3197955' # BSC 上的 USDT 代币合约地址
|
|
|
+IN_TOKEN_ADDRESS_BSC = '0x55d398326f99059ff775485246999027b3197955'
|
|
|
OUT_TOKEN_ADDRESS_BSC = '0x5C85D6C6825aB4032337F11Ee92a72DF936b46F6'
|
|
|
-AMOUNT_TO_QUERY_HUMAN = decimal.Decimal('1000') # 查询数量设置为1个单位的输入代币
|
|
|
+AMOUNT_TO_QUERY_HUMAN = decimal.Decimal('1000')
|
|
|
PROXY_HOST = '127.0.0.1'
|
|
|
PROXY_PORT = '7890'
|
|
|
proxies = {
|
|
|
'http': f'http://{PROXY_HOST}:{PROXY_PORT}',
|
|
|
'https': f'http://{PROXY_HOST}:{PROXY_PORT}',
|
|
|
}
|
|
|
+# proxies = None # 如果你不需要代理
|
|
|
+
|
|
|
+decimal.getcontext().prec = 36
|
|
|
|
|
|
|
|
|
-# --- OpenOcean 的价格获取函数 ---
|
|
|
+# --- 价格获取函数 (与之前相同, 省略以保持简洁) ---
|
|
|
def get_openocean_price_bsc(in_token_addr, out_token_addr, human_amount_in_decimal_for_request, gas_price=3):
|
|
|
- """
|
|
|
- 从 OpenOcean API 获取报价,并根据 API 返回的原子单位数量和代币小数位数计算价格。
|
|
|
-
|
|
|
- 价格计算方式为: (通过 inAmount 和 inToken.decimals 计算得到的人类可读输入数量) / (通过 outAmount 和 outToken.decimals 计算得到的人类可读输出数量)
|
|
|
- 这个价格表示 “每单位输出代币需要多少单位输入代币”。
|
|
|
-
|
|
|
- Args:
|
|
|
- in_token_addr (str): 输入代币的合约地址。
|
|
|
- out_token_addr (str): 输出代币的合约地址。
|
|
|
- human_amount_in_decimal_for_request (decimal.Decimal): 用于 API 请求的人类可读的输入金额。
|
|
|
- API会根据此金额和实际代币信息处理请求。
|
|
|
- gas_price (int, optional): Gas 价格。 默认为 3。
|
|
|
-
|
|
|
- Returns:
|
|
|
- dict: 包含价格计算结果或错误信息的字典。
|
|
|
- 成功时: {"price_in_per_out": calculated_price}
|
|
|
- 失败时: {"error": "错误信息"}
|
|
|
- """
|
|
|
+ # ... (代码与之前相同)
|
|
|
chain = 'bsc'
|
|
|
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), # API 请求中的 amount 参数
|
|
|
+ 'amount': str(human_amount_in_decimal_for_request),
|
|
|
'gasPrice': gas_price,
|
|
|
}
|
|
|
try:
|
|
|
response = requests.get(url, params=params, proxies=proxies, timeout=10)
|
|
|
- response.raise_for_status() # 如果HTTP状态码表示错误,则抛出异常
|
|
|
+ response.raise_for_status()
|
|
|
data = response.json()
|
|
|
|
|
|
if data.get('code') == 200 and data.get('data'):
|
|
|
api_data = data['data']
|
|
|
-
|
|
|
- # 确保所有需要的数据都存在
|
|
|
if not (api_data.get('inToken') and api_data['inToken'].get('decimals') is not None and
|
|
|
api_data.get('outToken') and api_data['outToken'].get('decimals') is not None and
|
|
|
api_data.get('inAmount') is not None and api_data.get('outAmount') is not None):
|
|
|
- return {"error": "API响应中缺少必要的代币信息、数量或小数位数"}
|
|
|
+ return {"error": "OO API响应中缺少必要的数据"}
|
|
|
|
|
|
- # 从 API 响应中获取原子单位的数量和小数位数
|
|
|
in_token_decimals = int(api_data['inToken']['decimals'])
|
|
|
out_token_decimals = int(api_data['outToken']['decimals'])
|
|
|
-
|
|
|
atomic_in_amount = decimal.Decimal(api_data['inAmount'])
|
|
|
atomic_out_amount = decimal.Decimal(api_data['outAmount'])
|
|
|
|
|
|
- # 计算人类可读的输入数量
|
|
|
- # derived_human_in_amount = atomic_in_amount / (decimal.Decimal('10') ** in_token_decimals)
|
|
|
- # 根据您的反馈,OpenOcean的 inToken.volume 已经是人类可读的输入量,而 inAmount 是API内部处理后的原子单位量
|
|
|
- # 如果您的意图是严格使用API返回的 inAmount 来反推人类可读输入量,那么上面的计算是正确的。
|
|
|
- # 我们将基于 inAmount 和 outAmount 来计算,正如您所要求的。
|
|
|
derived_human_in_amount = atomic_in_amount / (decimal.Decimal('10') ** in_token_decimals)
|
|
|
-
|
|
|
- # 计算人类可读的输出数量
|
|
|
derived_human_out_amount = atomic_out_amount / (decimal.Decimal('10') ** out_token_decimals)
|
|
|
|
|
|
- if derived_human_out_amount == 0: # 避免除以零的错误
|
|
|
- return {"error": "计算得出的人类可读输出数量为零,无法计算价格"}
|
|
|
+ if derived_human_out_amount == 0:
|
|
|
+ return {"error": "OO 计算的输出数量为零"}
|
|
|
|
|
|
- if derived_human_in_amount == 0 and human_amount_in_decimal_for_request > 0:
|
|
|
- # 如果API返回的inAmount为0,但请求量大于0,可能是一个问题或特殊情况
|
|
|
- # 但我们仍按您的要求基于derived_human_in_amount计算
|
|
|
- pass
|
|
|
-
|
|
|
- # 根据您的要求:价格 = 输入数量 / 输出数量
|
|
|
- # 这个价格表示:1 单位的输出代币等价于多少单位的输入代币
|
|
|
calculated_price = derived_human_in_amount / derived_human_out_amount
|
|
|
-
|
|
|
- return {"price_in_per_out": calculated_price} # 使用更能描述价格含义的键名
|
|
|
-
|
|
|
+ return {"price_in_per_out": calculated_price}
|
|
|
else:
|
|
|
- # API 返回的 code 不是 200 或没有 data 字段
|
|
|
- error_message = data.get('message', 'N/A') if data else '无响应数据 (data is None)'
|
|
|
+ error_message = data.get('message', 'N/A') if data else '无响应数据'
|
|
|
error_code = data.get('code', 'N/A') if data else 'N/A'
|
|
|
return {"error": f"OO API 错误 - Code: {error_code}, Msg: {error_message}"}
|
|
|
-
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
return {"error": f"OO请求失败: {e}"}
|
|
|
- except Exception as e: # 捕获包括 JSONDecodeError 在内的其他潜在错误
|
|
|
+ except Exception as e:
|
|
|
return {"error": f"OO意外错误: {e}"}
|
|
|
|
|
|
|
|
|
def get_gateio_spot_price(pair_symbol):
|
|
|
+ # ... (代码与之前相同)
|
|
|
url = f'https://api.gateio.ws/api/v4/spot/tickers'
|
|
|
params = {'currency_pair': pair_symbol}
|
|
|
try:
|
|
|
@@ -127,86 +94,154 @@ def get_gateio_spot_price(pair_symbol):
|
|
|
return {"error": f"Gate意外错误: {e}"}
|
|
|
|
|
|
|
|
|
-# --- 主逻辑 (已修改) ---
|
|
|
-def main():
|
|
|
- print(f"开始轮询价格 (每秒一次), BSC vs Gate.io {GATEIO_SPOT_PAIR}")
|
|
|
- print("按 Ctrl+C 停止。")
|
|
|
- # 打印表头,使用格式化字符串使其对齐
|
|
|
- header = f"{'OpenOcean ':<25} | {'Gate.io ':<25} | {'价差百分比':<15}"
|
|
|
- print(header)
|
|
|
- print("-" * (25 + 3 + 25 + 3 + 15)) # 打印与表头长度匹配的分隔线
|
|
|
+app = Flask(__name__)
|
|
|
|
|
|
- try:
|
|
|
- while True:
|
|
|
- # --- 初始化本轮迭代的变量 ---
|
|
|
- oo_rate_usdc_per_usdt = None # OpenOcean 的汇率 (1 USDT = X USDC)
|
|
|
- gate_rate_usdc_per_usdt_inverted = None # Gate.io 转换后的汇率 (1 USDT = X USDC)
|
|
|
-
|
|
|
- oo_display_str = "N/A" # OpenOcean 在最终输出行中显示的字符串
|
|
|
- gate_display_str = "N/A" # Gate.io 在最终输出行中显示的字符串
|
|
|
- diff_percentage_display_str = "N/A" # 价差百分比在最终输出行中显示的字符串
|
|
|
-
|
|
|
- oo_error_this_iteration = None # 存储本轮OpenOcean的错误信息
|
|
|
- gate_error_this_iteration = None # 存储本轮Gate.io的错误信息
|
|
|
-
|
|
|
- # --- 1. OpenOcean BSC 查询 ---
|
|
|
- oo_price_data = get_openocean_price_bsc(
|
|
|
- IN_TOKEN_ADDRESS_BSC,
|
|
|
- OUT_TOKEN_ADDRESS_BSC,
|
|
|
- AMOUNT_TO_QUERY_HUMAN
|
|
|
- )
|
|
|
-
|
|
|
- if "error" not in oo_price_data:
|
|
|
- oo_rate_usdc_per_usdt = oo_price_data['price_in_per_out']
|
|
|
- oo_display_str = f"{oo_rate_usdc_per_usdt:.6f}" # 格式化价格
|
|
|
- else:
|
|
|
- oo_error_this_iteration = oo_price_data['error'] # 记录错误信息
|
|
|
-
|
|
|
- # --- 2. Gate.io 现货查询 ---
|
|
|
- gate_price_data = get_gateio_spot_price(GATEIO_SPOT_PAIR)
|
|
|
-
|
|
|
- if "error" not in gate_price_data:
|
|
|
- gate_rate_usdt_per_usdc = gate_price_data['price_base_in_quote']
|
|
|
- if gate_rate_usdt_per_usdc is not None and gate_rate_usdt_per_usdc > 0:
|
|
|
- # 转换 Gate.io 的汇率方向为 1 USDT = Y USDC (Y = 1 / X)
|
|
|
- gate_rate_usdc_per_usdt_inverted = gate_rate_usdt_per_usdc
|
|
|
- gate_display_str = f"{gate_rate_usdc_per_usdt_inverted:.6f}" # 格式化价格
|
|
|
- elif gate_rate_usdt_per_usdc is not None: # 价格为0或负数
|
|
|
- gate_error_this_iteration = f"Gate.io 无效汇率 ({gate_rate_usdt_per_usdc})"
|
|
|
- gate_display_str = "Invalid" # 在行内显示为无效
|
|
|
- # else: price_base_in_quote is None, error already handled by "error" key check
|
|
|
- else:
|
|
|
- gate_error_this_iteration = gate_price_data['error'] # 记录错误信息
|
|
|
-
|
|
|
- # --- 3. 计算价差百分比 (仅当两边价格都有效时) ---
|
|
|
- if oo_rate_usdc_per_usdt is not None and gate_rate_usdc_per_usdt_inverted is not None:
|
|
|
- if gate_rate_usdc_per_usdt_inverted != 0: # 避免除以零
|
|
|
- # 价差 = OpenOcean汇率 - Gate.io转换后汇率
|
|
|
- difference = oo_rate_usdc_per_usdt - gate_rate_usdc_per_usdt_inverted
|
|
|
- # 价差百分比 = (价差 / Gate.io转换后汇率) * 100
|
|
|
- # 您可以根据需要选择以哪个价格为基准计算百分比,这里以 Gate.io 为基准
|
|
|
- percentage_diff = (difference / gate_rate_usdc_per_usdt_inverted) * 100
|
|
|
- diff_percentage_display_str = f"{percentage_diff:+.4f}%" # 显示正负号和小数点后4位
|
|
|
- else:
|
|
|
- diff_percentage_display_str = "Gate.io汇率为0" # Gate.io 汇率为0,无法计算百分比
|
|
|
-
|
|
|
- # --- 4. 打印错误信息 (如果本轮有错误发生) ---
|
|
|
- # 这些错误会打印在数据行的上方,以便用户了解具体问题
|
|
|
- if oo_error_this_iteration:
|
|
|
- print(f"[错误] OpenOcean: {oo_error_this_iteration}")
|
|
|
- if gate_error_this_iteration:
|
|
|
- print(f"[错误] Gate.io: {gate_error_this_iteration}")
|
|
|
-
|
|
|
- # --- 5. 组合打印在一行 ---
|
|
|
- # 使用格式化字符串确保列对齐
|
|
|
- print(f"{oo_display_str:<25} | {gate_display_str:<25} | {diff_percentage_display_str:<15}")
|
|
|
-
|
|
|
- time.sleep(1) # 等待1秒
|
|
|
+# --- 全局数据和历史记录 ---
|
|
|
+MAX_HISTORY_POINTS = 60 # 图表上显示的最大数据点数量 (例如,过去5分钟,每5秒一个点)
|
|
|
+latest_data = {
|
|
|
+ "oo_price": None,
|
|
|
+ "gate_price": None,
|
|
|
+ "difference_percentage": None,
|
|
|
+ "oo_error": None,
|
|
|
+ "gate_error": None,
|
|
|
+ "last_updated": None
|
|
|
+}
|
|
|
+# 使用 deque 来存储历史数据,它在添加和删除元素时具有 O(1) 的复杂度
|
|
|
+historical_prices = deque(maxlen=MAX_HISTORY_POINTS) # 存储 {'timestamp': ts, 'oo': price, 'gate': price}
|
|
|
+historical_diff = deque(maxlen=MAX_HISTORY_POINTS) # 存储 {'timestamp': ts, 'diff': percentage}
|
|
|
+
|
|
|
+data_lock = threading.Lock()
|
|
|
+
|
|
|
+
|
|
|
+def update_prices_periodically():
|
|
|
+ global latest_data, historical_prices, historical_diff
|
|
|
+ while True:
|
|
|
+ fetch_timestamp_str = time.strftime("%H:%M:%S") # 用于图表标签的简单时间戳
|
|
|
+
|
|
|
+ oo_price_data = get_openocean_price_bsc(
|
|
|
+ IN_TOKEN_ADDRESS_BSC,
|
|
|
+ OUT_TOKEN_ADDRESS_BSC,
|
|
|
+ AMOUNT_TO_QUERY_HUMAN
|
|
|
+ )
|
|
|
+ gate_price_data = get_gateio_spot_price(GATEIO_SPOT_PAIR)
|
|
|
+
|
|
|
+ current_oo_price_decimal = None
|
|
|
+ current_gate_price_decimal = None
|
|
|
+ current_oo_error = None
|
|
|
+ current_gate_error = None
|
|
|
+ diff_percentage_val = None
|
|
|
+ diff_percentage_str = "N/A"
|
|
|
+
|
|
|
+ if "error" not in oo_price_data:
|
|
|
+ current_oo_price_decimal = oo_price_data['price_in_per_out']
|
|
|
+ else:
|
|
|
+ current_oo_error = oo_price_data['error']
|
|
|
|
|
|
- except KeyboardInterrupt: # 允许用户通过 Ctrl+C 来停止脚本
|
|
|
- print("\n轮询停止。")
|
|
|
+ if "error" not in gate_price_data:
|
|
|
+ current_gate_price_decimal = gate_price_data['price_base_in_quote']
|
|
|
+ else:
|
|
|
+ current_gate_error = gate_price_data['error']
|
|
|
+
|
|
|
+ # 只有当两个价格都有效时,才计算价差并记录历史
|
|
|
+ if current_oo_price_decimal is not None and current_gate_price_decimal is not None:
|
|
|
+ # 将Decimal转换为浮点数以便存储和图表绘制 (Chart.js 通常使用JS number)
|
|
|
+ oo_price_float = float(current_oo_price_decimal)
|
|
|
+ gate_price_float = float(current_gate_price_decimal)
|
|
|
+
|
|
|
+ historical_prices.append({
|
|
|
+ "timestamp": fetch_timestamp_str,
|
|
|
+ "oo": oo_price_float,
|
|
|
+ "gate": gate_price_float
|
|
|
+ })
|
|
|
+
|
|
|
+ if current_gate_price_decimal > 0:
|
|
|
+ difference = current_oo_price_decimal - current_gate_price_decimal
|
|
|
+ percentage_diff_decimal = (difference / current_gate_price_decimal) * 100
|
|
|
+ diff_percentage_val = float(percentage_diff_decimal) # 用于图表
|
|
|
+ diff_percentage_str = f"{percentage_diff_decimal:+.4f}%" # 用于显示
|
|
|
+ historical_diff.append({
|
|
|
+ "timestamp": fetch_timestamp_str,
|
|
|
+ "diff": diff_percentage_val
|
|
|
+ })
|
|
|
+ else:
|
|
|
+ diff_percentage_str = "Gate价格为0"
|
|
|
+ # 对于价差图表,如果gate价格为0,可以记录一个特殊值或跳过
|
|
|
+ historical_diff.append({
|
|
|
+ "timestamp": fetch_timestamp_str,
|
|
|
+ "diff": None # 或者一个非常大/小的值来表示无效
|
|
|
+ })
|
|
|
+ else:
|
|
|
+ # 如果其中一个价格无效,我们仍然可以尝试记录有效的那个,或者都标记为None
|
|
|
+ # 这里我们选择如果任一价格无效,价格历史点也记录None,价差历史点也记录None
|
|
|
+ historical_prices.append({
|
|
|
+ "timestamp": fetch_timestamp_str,
|
|
|
+ "oo": float(current_oo_price_decimal) if current_oo_price_decimal else None,
|
|
|
+ "gate": float(current_gate_price_decimal) if current_gate_price_decimal else None
|
|
|
+ })
|
|
|
+ historical_diff.append({
|
|
|
+ "timestamp": fetch_timestamp_str,
|
|
|
+ "diff": None
|
|
|
+ })
|
|
|
+
|
|
|
+ with data_lock:
|
|
|
+ latest_data["oo_price"] = str(current_oo_price_decimal) if current_oo_price_decimal is not None else None
|
|
|
+ latest_data["gate_price"] = str(
|
|
|
+ current_gate_price_decimal) if current_gate_price_decimal is not None else None
|
|
|
+ latest_data["difference_percentage"] = diff_percentage_str
|
|
|
+ latest_data["oo_error"] = current_oo_error
|
|
|
+ latest_data["gate_error"] = current_gate_error
|
|
|
+ latest_data["last_updated"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
+
|
|
|
+ log_oo = f"{current_oo_price_decimal:.6f}" if current_oo_price_decimal else "N/A"
|
|
|
+ log_gate = f"{current_gate_price_decimal:.6f}" if current_gate_price_decimal else "N/A"
|
|
|
+ print(
|
|
|
+ f"Data updated at {fetch_timestamp_str}: OO: {log_oo}, Gate: {log_gate}, Diff: {diff_percentage_str}, Errors: OO: {current_oo_error}, Gate: {current_gate_error}")
|
|
|
+
|
|
|
+ time.sleep(5) # 更新频率
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/')
|
|
|
+def index():
|
|
|
+ return render_template('index.html')
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/data')
|
|
|
+def get_data():
|
|
|
+ with data_lock:
|
|
|
+ # 准备图表需要的数据
|
|
|
+ # 对于价格图表
|
|
|
+ price_chart_labels = [item['timestamp'] for item in historical_prices]
|
|
|
+ oo_price_values = [item['oo'] for item in historical_prices]
|
|
|
+ gate_price_values = [item['gate'] for item in historical_prices]
|
|
|
+
|
|
|
+ # 对于价差图表
|
|
|
+ diff_chart_labels = [item['timestamp'] for item in historical_diff]
|
|
|
+ diff_values = [item['diff'] for item in historical_diff]
|
|
|
+
|
|
|
+ # 将最新的数据点加入 (如果历史记录已满,deque会自动移除旧的)
|
|
|
+ # 这里的数据是 latest_data 和 historical_data 的组合
|
|
|
+ # Chart.js需要完整的历史数据来初始化,然后可以增量更新
|
|
|
+ # 或者,前端每次都获取完整的历史窗口
|
|
|
+
|
|
|
+ # 为了简单起见,前端将定期获取完整的 /data,其中包含历史和当前
|
|
|
+ response_data = {
|
|
|
+ "current": latest_data,
|
|
|
+ "history": {
|
|
|
+ "prices": {
|
|
|
+ "labels": list(price_chart_labels), # deque 不是直接 JSON 可序列化的
|
|
|
+ "oo": list(oo_price_values),
|
|
|
+ "gate": list(gate_price_values)
|
|
|
+ },
|
|
|
+ "difference": {
|
|
|
+ "labels": list(diff_chart_labels),
|
|
|
+ "values": list(diff_values)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return jsonify(response_data)
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
- decimal.getcontext().prec = 36 # 设置 decimal 的计算精度
|
|
|
- main()
|
|
|
+ price_updater_thread = threading.Thread(target=update_prices_periodically, daemon=True)
|
|
|
+ price_updater_thread.start()
|
|
|
+ app.run(debug=True, host='0.0.0.0', port=5000, use_reloader=False)
|