update polymarket trading: fix auth, add order monitoring, optimize status handling, remove unused scripts
This commit is contained in:
1 parent
7004b281a4
commit
f13b1c1afe
7 files changed
+378
-504
No files matched your search
+301
-67
@@ -150,18 +150,80 @@ class PolymarketBTCTrader:
|
||||
"""初始化CLOB交易客户端"""
|
||||
try:
|
||||
from py_clob_client.client import ClobClient
|
||||
from py_clob_client.clob_types import ApiCreds, OpenOrderParams
|
||||
|
||||
self.clob_client = ClobClient(
|
||||
host=self.CLOB_API,
|
||||
key=self.private_key,
|
||||
chain_id=137, # Polygon
|
||||
)
|
||||
self.clob_client.set_api_creds(self.clob_client.create_or_derive_api_creds())
|
||||
logger.info("✓ CLOB客户端初始化成功")
|
||||
funder = os.getenv("FUNDER_ADDRESS")
|
||||
nonce_str = os.getenv("POLYMARKET_NONCE") # 可选,取决于你的钱包设置
|
||||
nonce = int(nonce_str) if nonce_str else None
|
||||
if self.private_key and funder and nonce is not None:
|
||||
try:
|
||||
self.clob_client = ClobClient(
|
||||
host=self.CLOB_API,
|
||||
key=self.private_key,
|
||||
chain_id=137, # Polygon
|
||||
signature_type=2,
|
||||
funder=funder
|
||||
)
|
||||
derived_creds = self.clob_client.create_or_derive_api_creds(nonce=nonce)
|
||||
self.clob_client.set_api_creds(derived_creds)
|
||||
logger.info(f"✓ 私钥派生 API Creds 成功! Derived Key: {derived_creds.api_key}")
|
||||
# 验证API Key是否有效
|
||||
self.clob_client.get_api_keys()
|
||||
# 打印账户信息
|
||||
self._print_account_info()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 环境变量API凭证鉴权失败: {e}")
|
||||
self._print_account_info()
|
||||
else:
|
||||
self._print_account_info()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ CLOB客户端初始化失败: {e}")
|
||||
self.clob_client = None
|
||||
|
||||
def _print_account_info(self):
|
||||
"""打印账户余额和Allowance信息"""
|
||||
try:
|
||||
logger.info("-" * 40)
|
||||
logger.info("👤 账户信息检查:")
|
||||
|
||||
# 1. 获取地址
|
||||
address = self.clob_client.get_address()
|
||||
logger.info(f" 钱包地址: {address}")
|
||||
|
||||
# 2. 获取余额和Allowance
|
||||
try:
|
||||
from py_clob_client.clob_types import BalanceAllowanceParams, AssetType
|
||||
|
||||
# 检查 USDC (Collateral) 的余额
|
||||
# 在Polymarket中,USDC通常作为 COLLATERAL
|
||||
params = BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)
|
||||
|
||||
balance_allowance = self.clob_client.get_balance_allowance(params)
|
||||
logger.info(f" USDC余额: ${float(balance_allowance.get('balance', 0)) / 10**6:.2f}") # USDC是6位小数
|
||||
logger.info(f" 授权额度: {balance_allowance.get('allowance', 'N/A')}")
|
||||
|
||||
# 检查是否足够下单
|
||||
balance = float(balance_allowance.get('balance', 0)) / 10**6
|
||||
if balance < self.max_bet:
|
||||
logger.warning(f"⚠️ 余额不足: ${balance:.2f} < 下单额 ${self.max_bet:.2f}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f" 无法获取详细余额信息: {e}")
|
||||
# 备用方法
|
||||
try:
|
||||
logger.info(f" 尝试备用方法获取余额...")
|
||||
# 有些版本的 client 不需要 params
|
||||
ba = self.clob_client.get_balance_allowance()
|
||||
logger.info(f" Balance Info: {ba}")
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.info("-" * 40)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 获取账户信息失败: {e}")
|
||||
|
||||
def _log_to_db(self, result: Dict, market: Optional[Dict]):
|
||||
"""记录交易结果到数据库"""
|
||||
try:
|
||||
@@ -191,6 +253,7 @@ class PolymarketBTCTrader:
|
||||
'size': result.get('size'),
|
||||
'status': status,
|
||||
'error_message': result.get('error'),
|
||||
'order_id': result.get('order_id'),
|
||||
'raw_data': result
|
||||
}
|
||||
self.db.log_trade(log_data)
|
||||
@@ -379,6 +442,87 @@ class PolymarketBTCTrader:
|
||||
logger.debug(f"获取实时价格失败: {e}")
|
||||
return None
|
||||
|
||||
def monitor_order_status(self, order_id: str, timeout_seconds: Optional[int] = None) -> str:
|
||||
"""
|
||||
监控订单状态直到成交或超时
|
||||
timeout_seconds: 超时秒数。如果为None,默认监控直到当前小时结束前1分钟。
|
||||
"""
|
||||
if timeout_seconds is None:
|
||||
now = datetime.now()
|
||||
next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
|
||||
end_time = next_hour - timedelta(minutes=1)
|
||||
timeout_seconds = int((end_time - now).total_seconds())
|
||||
if timeout_seconds < 10:
|
||||
timeout_seconds = 10 # 至少监控10秒
|
||||
|
||||
logger.info(f"🕵️ 开始监控订单状态: {order_id} (超时: {timeout_seconds}s)")
|
||||
start_monitor = datetime.now()
|
||||
last_status = 'UNKNOWN'
|
||||
|
||||
while (datetime.now() - start_monitor).total_seconds() < timeout_seconds:
|
||||
try:
|
||||
order_data = self.clob_client.get_order(order_id)
|
||||
status = order_data.get('status')
|
||||
last_status = status
|
||||
|
||||
if status in ['MATCHED', 'FILLED']:
|
||||
logger.info(f"🎉 订单已成交! 状态: {status}")
|
||||
return status
|
||||
elif status == 'CANCELED':
|
||||
logger.warning(f"🚫 订单已取消")
|
||||
return status
|
||||
elif status == 'LIVE':
|
||||
logger.info(f" 订单挂单中 (LIVE)...")
|
||||
time.sleep(30)
|
||||
else:
|
||||
logger.info(f" 订单状态: {status} (等待成交...)")
|
||||
time.sleep(30) # 轮询间隔
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 查询订单状态失败: {e}")
|
||||
time.sleep(30)
|
||||
|
||||
logger.info(f"⚠️ 监控超时,当前状态: {last_status}")
|
||||
return last_status
|
||||
|
||||
def _log_to_db(self, result: Dict, market: Optional[Dict]):
|
||||
"""记录交易结果到数据库"""
|
||||
try:
|
||||
# 确定状态
|
||||
status = 'UNKNOWN'
|
||||
|
||||
if self.dry_run:
|
||||
status = 'test'
|
||||
else:
|
||||
# 真实交易:优先使用 Poly 订单状态
|
||||
# 如果没有 order_id,说明下单未成功发起,标记为 FAILED
|
||||
if not result.get('order_id'):
|
||||
status = 'FAILED'
|
||||
elif result.get('status'):
|
||||
status = result.get('status')
|
||||
else:
|
||||
status = 'SUBMITTED' # 有ID但无状态,暂时标记为已提交
|
||||
|
||||
log_data = {
|
||||
'timestamp': result.get('timestamp'),
|
||||
'market_title': result.get('market'),
|
||||
'market_slug': market.get('slug') if market else result.get('market_slug'),
|
||||
'model_name': 'Ensemble', # 记录为集成策略
|
||||
'prediction': result.get('prediction'),
|
||||
'probability': result.get('probability'),
|
||||
'action': result.get('side'),
|
||||
'price': result.get('price'),
|
||||
'amount': result.get('amount'),
|
||||
'size': result.get('size'),
|
||||
'status': status,
|
||||
'error_message': result.get('error'),
|
||||
'order_id': result.get('order_id'),
|
||||
'raw_data': result
|
||||
}
|
||||
self.db.log_trade(log_data)
|
||||
logger.info(f"💾 交易记录已保存到数据库 (Status: {status})")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 保存数据库失败: {e}")
|
||||
|
||||
def execute_trade(self, prediction: int, market: Dict) -> Dict:
|
||||
"""执行交易"""
|
||||
result = {
|
||||
@@ -388,25 +532,27 @@ class PolymarketBTCTrader:
|
||||
'success': False,
|
||||
'order': None,
|
||||
'error': None,
|
||||
'order_id': None,
|
||||
'status': None
|
||||
}
|
||||
|
||||
if prediction == 1: # 预测涨 -> 买UP
|
||||
token_id = market['tokens'][0]
|
||||
price = market['outcome_prices'][0]
|
||||
price = 0.5 # 暂时固定价格
|
||||
side = "UP"
|
||||
else: # 预测跌 -> 买DOWN
|
||||
token_id = market['tokens'][1]
|
||||
price = market['outcome_prices'][1]
|
||||
price = 0.5 # 暂时固定价格
|
||||
side = "DOWN"
|
||||
|
||||
result['side'] = side
|
||||
result['price'] = price
|
||||
|
||||
# 检查价格限制
|
||||
if price > self.max_price:
|
||||
result['error'] = f"价格${price:.2f}超过限制${self.max_price:.2f}"
|
||||
logger.error(f"❌ {result['error']}")
|
||||
return result
|
||||
# if price > self.max_price:
|
||||
# result['error'] = f"价格${price:.2f}超过限制${self.max_price:.2f}"
|
||||
# logger.error(f"❌ {result['error']}")
|
||||
# return result
|
||||
|
||||
# 计算数量
|
||||
size = self.max_bet / price
|
||||
@@ -423,6 +569,7 @@ class PolymarketBTCTrader:
|
||||
logger.info("🔸 [模拟模式] 未实际下单")
|
||||
result['success'] = True
|
||||
result['order'] = 'DRY_RUN'
|
||||
result['status'] = 'simulated'
|
||||
return result
|
||||
|
||||
# 实际下单
|
||||
@@ -433,21 +580,60 @@ class PolymarketBTCTrader:
|
||||
|
||||
try:
|
||||
from py_clob_client.order_builder.constants import BUY
|
||||
from py_clob_client.clob_types import OrderArgs, OpenOrderParams
|
||||
|
||||
order = self.clob_client.create_limit_order(
|
||||
token_id=token_id,
|
||||
price=price,
|
||||
size=size,
|
||||
side=BUY,
|
||||
)
|
||||
|
||||
response = self.clob_client.post_order(order)
|
||||
logger.info(f"✓ 订单已提交: {response}")
|
||||
result['success'] = True
|
||||
result['order'] = response
|
||||
# 2. 检查现有挂单
|
||||
order_id = None
|
||||
try:
|
||||
open_order_params = OpenOrderParams(asset_id=token_id)
|
||||
existing_orders = self.clob_client.get_orders(open_order_params)
|
||||
if existing_orders:
|
||||
existing_order = existing_orders[0]
|
||||
order_id = existing_order.get('orderID')
|
||||
logger.warning(f"⚠️ 检测到已有挂单: {order_id},跳过下单")
|
||||
result['order_id'] = order_id
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 检查挂单失败 (不影响下单): {e}")
|
||||
|
||||
if not order_id:
|
||||
# 创建订单参数
|
||||
order_args = OrderArgs(
|
||||
price=price,
|
||||
size=size,
|
||||
side=BUY,
|
||||
token_id=token_id,
|
||||
)
|
||||
|
||||
# 3. 原子化下单 (Create & Post)
|
||||
logger.info(f"正在下单... (Price: {price}, Size: {size})")
|
||||
response = self.clob_client.create_and_post_order(order_args)
|
||||
|
||||
if response and response.get('success'):
|
||||
order_id = response.get('orderID')
|
||||
result['order_id'] = order_id
|
||||
result['success'] = True
|
||||
result['order'] = response
|
||||
logger.info(f"✓ 订单已提交, Order ID: {order_id}")
|
||||
else:
|
||||
error_msg = response.get('errorMsg') if response else "未知错误"
|
||||
result['error'] = f"下单失败: {error_msg}"
|
||||
logger.error(f"❌ {result['error']}")
|
||||
logger.error(f" Response: {response}")
|
||||
|
||||
# 4. 立即查询状态 (无论是新单还是旧单)
|
||||
if order_id:
|
||||
try:
|
||||
order_status = self.clob_client.get_order(order_id)
|
||||
status = order_status.get('status')
|
||||
result['status'] = status
|
||||
logger.info(f" 订单状态: {status}")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 无法获取订单状态: {e}")
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
logger.error(f"❌ 下单失败: {e}")
|
||||
logger.error(f"❌ 下单异常: {e}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -662,60 +848,108 @@ class PolymarketBTCTrader:
|
||||
return result
|
||||
|
||||
# 检查是否已交易过该市场
|
||||
check_status = 'test' if self.dry_run else 'success'
|
||||
if self.db.check_existing_trade(market['slug'], status=check_status):
|
||||
result['error'] = '该市场已存在成功交易记录'
|
||||
logger.warning(f"⚠️ {result['error']}, 跳过交易")
|
||||
return result
|
||||
existing_trade = self.db.get_existing_trade(market['slug'])
|
||||
|
||||
if existing_trade:
|
||||
status = existing_trade.get('status')
|
||||
order_id = existing_trade.get('order_id')
|
||||
|
||||
# 如果是成功状态或模拟测试,且不需要进一步同步,则直接跳过
|
||||
if status in ['test', 'MATCHED', 'FILLED']:
|
||||
# 但如果是 dry_run 模式,且 existing_trade 也是 test,则认为是已完成
|
||||
if self.dry_run and status == 'test':
|
||||
result['error'] = '该市场已存在模拟交易记录'
|
||||
logger.warning(f"⚠️ {result['error']}, 跳过交易")
|
||||
return result
|
||||
elif not self.dry_run and status in ['MATCHED', 'FILLED']:
|
||||
result['error'] = '该市场已存在成功交易记录'
|
||||
logger.warning(f"⚠️ {result['error']}, 跳过交易")
|
||||
return result
|
||||
|
||||
# 如果存在未完成的订单 (且不是 dry_run),尝试同步状态
|
||||
# FAILED 和 CANCELED 可以重试
|
||||
if not self.dry_run and order_id and status not in ['FAILED', 'CANCELED', 'test']:
|
||||
logger.info(f"🔄 发现已有订单 ({status}), 正在同步状态... OrderID: {order_id}")
|
||||
try:
|
||||
# 使用较短的超时时间进行一次快速检查
|
||||
new_status = self.monitor_order_status(order_id, timeout_seconds=10)
|
||||
|
||||
if new_status != status:
|
||||
self.db.update_trade_status(existing_trade['id'], new_status)
|
||||
logger.info(f"📝 数据库状态已更新: {status} -> {new_status}")
|
||||
|
||||
if new_status in ['MATCHED', 'FILLED']:
|
||||
result['status'] = new_status
|
||||
result['error'] = '该市场已存在成功交易记录 (已同步)'
|
||||
return result
|
||||
elif new_status == 'CANCELED':
|
||||
logger.warning("⚠️ 原订单已取消,将尝试重新下单...")
|
||||
# 允许后续逻辑继续执行,尝试重新下单
|
||||
pass
|
||||
else:
|
||||
logger.info(f"⏳ 订单状态仍为 {new_status},继续等待或根据策略处理...")
|
||||
# 如果还在 OPEN/LIVE,可能需要取消重下或者继续等待?
|
||||
# 这里简单处理:如果还在挂单中,暂不重复下单
|
||||
if new_status in ['OPEN', 'LIVE', 'pending']:
|
||||
result['error'] = f'该市场已存在挂单 ({new_status})'
|
||||
result['order_id'] = order_id
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 同步订单状态失败: {e}")
|
||||
|
||||
# 3. 循环检查价格并交易
|
||||
start_time = datetime.now()
|
||||
end_time = start_time + timedelta(minutes=timeout_minutes)
|
||||
# start_time = datetime.now()
|
||||
# end_time = start_time + timedelta(minutes=timeout_minutes)
|
||||
|
||||
logger.info(f"🔄 开始价格监控 (预测: {'UP' if prediction == 1 else 'DOWN'})...")
|
||||
# logger.info(f"🔄 开始价格监控 (预测: {'UP' if prediction == 1 else 'DOWN'})...")
|
||||
|
||||
while datetime.now() < end_time:
|
||||
# while datetime.now() < end_time:
|
||||
# 刷新价格
|
||||
market = self.refresh_prices()
|
||||
if not market:
|
||||
logger.warning("无法获取市场价格,重试中...")
|
||||
time.sleep(10)
|
||||
continue
|
||||
# market = self.refresh_prices()
|
||||
# if not market:
|
||||
# logger.warning("无法获取市场价格,重试中...")
|
||||
# time.sleep(10)
|
||||
# continue
|
||||
|
||||
# 检查价格
|
||||
price_idx = 0 if prediction == 1 else 1
|
||||
current_price = market['outcome_prices'][price_idx]
|
||||
# price_idx = 0 if prediction == 1 else 1
|
||||
# current_price = market['outcome_prices'][price_idx]
|
||||
|
||||
logger.info(f" 当前价格: ${current_price:.3f} (目标: <= ${self.max_price:.2f})")
|
||||
# logger.info(f" 当前价格: ${current_price:.3f} (目标: <= ${self.max_price:.2f})")
|
||||
|
||||
if current_price <= self.max_price:
|
||||
# if current_price <= self.max_price:
|
||||
# 价格符合,执行交易
|
||||
logger.info("✅ 价格满足条件,正在下单...")
|
||||
trade_result = self.execute_trade(prediction, market)
|
||||
result.update(trade_result)
|
||||
|
||||
# 记录数据库
|
||||
self._log_to_db(result, market)
|
||||
|
||||
logger.info("=" * 60)
|
||||
return result
|
||||
else:
|
||||
# 价格过高,等待
|
||||
wait_time = 30 # 等待30秒
|
||||
elapsed = int((datetime.now() - start_time).total_seconds())
|
||||
logger.info(f"⏳ 价格过高,继续等待... (已耗时: {elapsed}s)")
|
||||
time.sleep(wait_time)
|
||||
logger.info("✅ 预测完成,正在下单(限价 0.5)...")
|
||||
trade_result = self.execute_trade(prediction, market)
|
||||
result.update(trade_result)
|
||||
|
||||
# 如果下单成功且有order_id,进入状态监控
|
||||
if result.get('success') and result.get('order_id'):
|
||||
final_status = self.monitor_order_status(result['order_id'])
|
||||
result['status'] = final_status
|
||||
|
||||
# 记录数据库
|
||||
self._log_to_db(result, market)
|
||||
|
||||
logger.info("=" * 60)
|
||||
return result
|
||||
# else:
|
||||
# # 价格过高,等待
|
||||
# wait_time = 30 # 等待30秒
|
||||
# elapsed = int((datetime.now() - start_time).total_seconds())
|
||||
# logger.info(f"⏳ 价格过高,继续等待... (已耗时: {elapsed}s)")
|
||||
# time.sleep(wait_time)
|
||||
|
||||
# 超时
|
||||
result['error'] = f'等待价格超时 ({timeout_minutes}分钟)'
|
||||
result['price'] = market['outcome_prices'][0] if prediction == 1 else market['outcome_prices'][1]
|
||||
result['side'] = 'UP' if prediction == 1 else 'DOWN'
|
||||
logger.error(f"❌ {result['error']}")
|
||||
# result['error'] = f'等待价格超时 ({timeout_minutes}分钟)'
|
||||
# result['price'] = market['outcome_prices'][0] if prediction == 1 else market['outcome_prices'][1]
|
||||
# result['side'] = 'UP' if prediction == 1 else 'DOWN'
|
||||
# logger.error(f"❌ {result['error']}")
|
||||
|
||||
self._log_to_db(result, market)
|
||||
logger.info("=" * 60)
|
||||
# self._log_to_db(result, market)
|
||||
# logger.info("=" * 60)
|
||||
|
||||
return result
|
||||
# return result
|
||||
|
||||
def _show_initial_prediction(self):
|
||||
"""
|
||||
@@ -790,9 +1024,9 @@ def main():
|
||||
help='模拟模式(不实际下单)')
|
||||
parser.add_argument('--real', action='store_true',
|
||||
help='真实交易模式')
|
||||
parser.add_argument('--max-bet', type=float, default=1.0,
|
||||
parser.add_argument('--max-bet', type=float, default=2.5,
|
||||
help='最大下注金额(USDC)')
|
||||
parser.add_argument('--max-price', type=float, default=0.50,
|
||||
parser.add_argument('--max-price', type=float, default=0.5,
|
||||
help='最高可接受价格')
|
||||
parser.add_argument('--once', action='store_true',
|
||||
help='只执行一次(不启动定时器)')
|
||||
|
||||
Reference in new issue
Block a user