Files
lm_code/交易/bitmart-返佣策略.py

764 lines
30 KiB
Python
Raw Permalink Normal View History

2026-02-15 02:16:45 +08:00
"""
BitMart EMA趋势跟随返佣策略
核心思路
你有交易所 90% 手续费返佣因此每笔交易的"真实成本"极低0.012%/round trip
策略使用 EMA 金叉/死叉 捕捉 1 分钟级别的微趋势配合大级别 EMA 趋势过滤
ATR 波动率过滤只在高波动顺势环境中开仓
持仓必须 >= 3 分钟避免被判定为刷量
回测表现2025全年 1 分钟 K 线
- 参数EMA(8/21/120) ATR>0.3% SL=0.4% MaxHold=1800s
- Risk=2% 年化 +10.11%最大回撤 10.92%
- Risk=3% 年化 +13.66%最大回撤 15.96%
- 全年约 227 笔交易平均 1.6 1
- 胜率 ~29%盈亏比 ~2.7:1低胜率高赔率模式
策略规则
1. 使用 1 分钟 K 线计算 EMA(8)快线EMA(21)慢线EMA(120)大趋势线
2. 计算 ATR(14) 波动率仅在 ATR > 0.3% 时交易过滤低波动区间
3. 开仓信号
- 做多EMA(8) 上穿 EMA(21) 价格 > EMA(120)顺势金叉
- 做空EMA(8) 下穿 EMA(21) 价格 < EMA(120)顺势死叉
4. 平仓条件
a) 反向交叉信号满足最低持仓后可同时反手开仓
b) 止损浮亏 >= 0.4%硬止损 0.6%不受持仓时间限制
c) 超时持仓 >= 30 分钟强制平仓
5. 平仓后若有反向信号且满足 ATR 过滤立即反手开仓
"""
import time
import uuid
import datetime
from loguru import logger
from bitmart.api_contract import APIContract
from bitmart.lib.cloud_exceptions import APIException
from 交易.tools import send_dingtalk_message
# ========================= EMA 计算器 =========================
class EMACalculator:
"""指数移动平均线,增量式更新"""
def __init__(self, period: int):
self.period = period
self.k = 2.0 / (period + 1)
self.value = None
def update(self, price: float) -> float:
if self.value is None:
self.value = price
else:
self.value = price * self.k + self.value * (1 - self.k)
return self.value
def reset(self):
self.value = None
class BitmartRebateStrategy:
def __init__(self):
self.api_key = "a0fb7b98464fd9bcce67e7c519d58ec10d0c38a8"
self.secret_key = "4eaeba78e77aeaab1c2027f846a276d164f264a44c2c1bb1c5f3be50c8de1ca5"
self.memo = "合约交易"
self.contract_symbol = "ETHUSDT"
self.contractAPI = APIContract(self.api_key, self.secret_key, self.memo, timeout=(5, 15))
# ========================= 持仓状态 =========================
self.start = 0 # -1 空, 0 无, 1 多
self.open_avg_price = None
self.current_amount = None
self.position_cross = None
self.open_time = None # 开仓时间戳(用于计算持仓时长)
# ========================= 杠杆 & 仓位 =========================
self.leverage = "50" # 杠杆倍数
self.open_type = "cross" # 全仓模式
self.risk_percent = 0.02 # 每次开仓使用可用余额的 2%(回测最优)
# ========================= EMA 参数(回测最优) =========================
self.ema_fast_period = 8 # 快线 EMA 周期
self.ema_slow_period = 21 # 慢线 EMA 周期
self.ema_big_period = 120 # 大趋势 EMA 周期2小时
self.atr_period = 14 # ATR 周期
self.atr_min_pct = 0.003 # ATR 最低阈值 0.3%(过滤低波动)
# ========================= 持仓管理参数(回测最优) =========================
self.min_hold_seconds = 200 # 最低持仓时间(秒),>3分钟
self.max_hold_seconds = 1800 # 最长持仓时间30分钟
self.stop_loss_pct = 0.004 # 止损百分比 0.4%
self.hard_stop_pct = 0.006 # 硬止损 0.6%(不受时间限制)
# ========================= EMA 状态 =========================
self.ema_fast = EMACalculator(self.ema_fast_period)
self.ema_slow = EMACalculator(self.ema_slow_period)
self.ema_big = EMACalculator(self.ema_big_period)
self.prev_fast = None # 上一根K线的快线值
self.prev_slow = None # 上一根K线的慢线值
self.pending_signal = None # 等待最低持仓后执行的延迟信号
# ========================= K线缓存用于 ATR 计算) =========================
self.highs = []
self.lows = []
self.closes = []
self.last_kline_time = None # 最新处理过的K线时间戳
# ========================= 运行控制 =========================
self.check_interval = 5 # 主循环检测间隔(秒)
# ========================= 统计 =========================
self.trade_count = 0 # 当日交易次数
self.total_pnl = 0.0 # 当日累计盈亏
self.total_volume = 0.0 # 当日累计交易额(用于估算返佣)
self.start_time = time.time() # 程序启动时间
# ========================= 技术指标计算 =========================
def calculate_atr_pct(self, current_price):
"""计算 ATR 占当前价格的百分比"""
if len(self.highs) < self.atr_period + 1:
return 0.0
trs = []
for i in range(-self.atr_period, 0):
h = self.highs[i]
l = self.lows[i]
pc = self.closes[i - 1]
tr = max(h - l, abs(h - pc), abs(l - pc))
trs.append(tr)
atr = sum(trs) / self.atr_period
return atr / current_price if current_price > 0 else 0.0
def detect_cross(self, fast_val, slow_val):
"""
检测 EMA 金叉/死叉
返回: 'golden' (金叉) / 'death' (死叉) / None
"""
if self.prev_fast is None or self.prev_slow is None:
return None
# 金叉:快线从下方穿越慢线
if self.prev_fast <= self.prev_slow and fast_val > slow_val:
return "golden"
# 死叉:快线从上方穿越慢线
if self.prev_fast >= self.prev_slow and fast_val < slow_val:
return "death"
return None
def process_new_kline(self, kline):
"""
处理新的 1 分钟 K 线更新所有指标
返回: (cross_signal, atr_pct, fast, slow, big)
"""
close = kline['close']
high = kline['high']
low = kline['low']
# 缓存 K 线数据
self.highs.append(high)
self.lows.append(low)
self.closes.append(close)
# 只保留最近 200 根(节省内存)
if len(self.highs) > 200:
self.highs = self.highs[-200:]
self.lows = self.lows[-200:]
self.closes = self.closes[-200:]
# 更新 EMA
fast_val = self.ema_fast.update(close)
slow_val = self.ema_slow.update(close)
big_val = self.ema_big.update(close)
# 检测交叉
cross = self.detect_cross(fast_val, slow_val)
# 保存当前值供下次对比
self.prev_fast = fast_val
self.prev_slow = slow_val
# 计算 ATR
atr_pct = self.calculate_atr_pct(close)
return cross, atr_pct, fast_val, slow_val, big_val
# ========================= 数据获取 =========================
def get_1min_klines(self, count=150):
"""获取最近 N 根 1 分钟 K 线"""
try:
end_time = int(time.time())
start_time = end_time - 60 * count * 2 # 多取一些保证够用
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1, # 1 分钟
start_time=start_time,
end_time=end_time
)[0]
if response['code'] != 1000:
logger.error(f"获取K线失败: {response}")
return None
formatted = []
for k in response['data']:
formatted.append({
'timestamp': int(k["timestamp"]),
'open': float(k["open_price"]),
'high': float(k["high_price"]),
'low': float(k["low_price"]),
'close': float(k["close_price"]),
})
formatted.sort(key=lambda x: x['timestamp'])
# 只保留最近 count 根
return formatted[-count:]
except Exception as e:
logger.error(f"获取1分钟K线异常: {e}")
return None
def get_current_price(self):
"""获取当前最新价格"""
try:
end_time = int(time.time())
response = self.contractAPI.get_kline(
contract_symbol=self.contract_symbol,
step=1,
start_time=end_time - 300,
end_time=end_time
)[0]
if response['code'] == 1000 and response['data']:
return float(response['data'][-1]["close_price"])
return None
except Exception as e:
logger.error(f"获取价格异常: {e}")
return None
def get_available_balance(self):
"""获取合约账户可用 USDT 余额"""
try:
response = self.contractAPI.get_assets_detail()[0]
if response['code'] == 1000:
data = response['data']
if isinstance(data, dict):
return float(data.get('available_balance', 0))
elif isinstance(data, list):
for asset in data:
if asset.get('currency') == 'USDT':
return float(asset.get('available_balance', 0))
return None
except Exception as e:
logger.error(f"余额查询异常: {e}")
return None
def get_position_status(self):
"""获取当前持仓状态"""
try:
response = self.contractAPI.get_position(contract_symbol=self.contract_symbol)[0]
if response['code'] == 1000:
positions = response['data']
if not positions:
self.start = 0
self.open_avg_price = None
self.current_amount = None
self.position_cross = None
return True
self.start = 1 if positions[0]['position_type'] == 1 else -1
self.open_avg_price = float(positions[0]['open_avg_price'])
self.current_amount = float(positions[0]['current_amount'])
self.position_cross = positions[0].get("position_cross")
return True
return False
except Exception as e:
logger.error(f"持仓查询异常: {e}")
return False
# ========================= 交易执行 =========================
def set_leverage(self):
"""设置全仓 + 杠杆"""
try:
response = self.contractAPI.post_submit_leverage(
contract_symbol=self.contract_symbol,
leverage=self.leverage,
open_type=self.open_type
)[0]
if response['code'] == 1000:
logger.success(f"全仓模式 + {self.leverage}x 杠杆设置成功")
return True
else:
logger.error(f"杠杆设置失败: {response}")
return False
except Exception as e:
logger.error(f"设置杠杆异常: {e}")
return False
def calculate_size(self, price):
"""计算开仓张数"""
balance = self.get_available_balance()
if not balance or balance < 10:
logger.warning(f"余额不足: {balance}")
return 0
leverage = int(self.leverage)
margin = balance * self.risk_percent
# ETHUSDT 1张 ≈ 0.001 ETH
size = int((margin * leverage) / (price * 0.001))
size = max(1, size)
logger.info(f"余额 {balance:.2f} USDT → 保证金 {margin:.2f} USDT → 开仓 {size} 张 (价格≈{price:.2f})")
return size
def place_market_order(self, side: int, size: int):
"""
下市价单
side: 1=开多, 2=平空, 3=平多, 4=开空
"""
if size <= 0:
return False
client_order_id = f"rebate_{int(time.time())}_{uuid.uuid4().hex[:8]}"
side_names = {1: "开多", 2: "平空", 3: "平多", 4: "开空"}
try:
response = self.contractAPI.post_submit_order(
contract_symbol=self.contract_symbol,
client_order_id=client_order_id,
side=side,
mode=1,
type='market',
leverage=self.leverage,
open_type=self.open_type,
size=size
)[0]
if response['code'] == 1000:
logger.success(f"下单成功: {side_names.get(side, side)} {size}")
return True
else:
logger.error(f"下单失败: {response}")
return False
except APIException as e:
logger.error(f"API下单异常: {e}")
return False
def open_position(self, direction: str, price: float):
"""开仓"""
size = self.calculate_size(price)
if size == 0:
return False
if direction == "long":
if self.place_market_order(1, size):
self.start = 1
self.open_avg_price = price
self.open_time = time.time()
self.current_amount = size
self.pending_signal = None
# 统计交易额
volume = size * 0.001 * price
self.total_volume += volume
self.trade_count += 1
logger.success(f"开多 {size} 张 @ {price:.2f}")
self.ding(f"开多 {size} 张 @ {price:.2f}")
return True
elif direction == "short":
if self.place_market_order(4, size):
self.start = -1
self.open_avg_price = price
self.open_time = time.time()
self.current_amount = size
self.pending_signal = None
volume = size * 0.001 * price
self.total_volume += volume
self.trade_count += 1
logger.success(f"开空 {size} 张 @ {price:.2f}")
self.ding(f"开空 {size} 张 @ {price:.2f}")
return True
return False
def close_position(self, reason: str, current_price: float):
"""平仓"""
if self.start == 0:
return False
close_side = 3 if self.start == 1 else 2 # 3=平多, 2=平空
direction_str = "" if self.start == 1 else ""
if self.place_market_order(close_side, 999999):
# 计算本次盈亏
if self.open_avg_price and self.current_amount:
if self.start == 1:
pnl = self.current_amount * 0.001 * (current_price - self.open_avg_price)
else:
pnl = self.current_amount * 0.001 * (self.open_avg_price - current_price)
self.total_pnl += pnl
# 统计平仓交易额
volume = self.current_amount * 0.001 * current_price
self.total_volume += volume
hold_seconds = time.time() - self.open_time if self.open_time else 0
logger.success(
f"{direction_str} @ {current_price:.2f} | "
f"原因: {reason} | 持仓 {hold_seconds:.0f}s | "
f"本次盈亏: {pnl:+.4f} USDT"
)
self.ding(
f"{direction_str} @ {current_price:.2f}\n"
f"原因: {reason}\n持仓 {hold_seconds:.0f}s\n"
f"本次盈亏: {pnl:+.4f} USDT"
)
self.start = 0
self.open_avg_price = None
self.open_time = None
self.current_amount = None
self.pending_signal = None
self.trade_count += 1
return True
return False
# ========================= 信号检测 =========================
def check_open_signal(self, current_price, cross, atr_pct, big_val):
"""
检查开仓信号
返回: 'long' / 'short' / None
"""
# ATR 过滤:波动率不足时不开仓
if atr_pct < self.atr_min_pct:
return None
# 金叉 + 价格在大EMA上方 → 做多
if cross == "golden" and current_price > big_val:
logger.info(
f"做多信号 | 金叉 + 价格 {current_price:.2f} > EMA120 {big_val:.2f} | ATR {atr_pct*100:.3f}%"
)
return "long"
# 死叉 + 价格在大EMA下方 → 做空
if cross == "death" and current_price < big_val:
logger.info(
f"做空信号 | 死叉 + 价格 {current_price:.2f} < EMA120 {big_val:.2f} | ATR {atr_pct*100:.3f}%"
)
return "short"
return None
def check_close_signal(self, current_price, cross, atr_pct, fast_val, slow_val, big_val):
"""
检查平仓信号
返回: (should_close: bool, reason: str, reverse_direction: str or None)
reverse_direction: 平仓后是否反手 ('long'/'short'/None)
"""
if self.start == 0 or not self.open_avg_price or not self.open_time:
return False, "", None
hold_seconds = time.time() - self.open_time
# 计算浮动盈亏
if self.start == 1:
loss_pct = (self.open_avg_price - current_price) / self.open_avg_price
else:
loss_pct = (current_price - self.open_avg_price) / self.open_avg_price
# ① 硬止损:不受持仓时间限制
if loss_pct >= self.hard_stop_pct:
return True, f"硬止损 (亏损 {loss_pct*100:.3f}%)", None
# ② 未满足最低持仓时间
if hold_seconds < self.min_hold_seconds:
# 记录延迟信号(等持仓时间到了再处理)
if self.start == 1 and cross == "death":
self.pending_signal = "close_long"
logger.info(f"检测到死叉但持仓时间不足,记录延迟信号 | 还需 {self.min_hold_seconds - hold_seconds:.0f}s")
elif self.start == -1 and cross == "golden":
self.pending_signal = "close_short"
logger.info(f"检测到金叉但持仓时间不足,记录延迟信号 | 还需 {self.min_hold_seconds - hold_seconds:.0f}s")
remaining = self.min_hold_seconds - hold_seconds
if int(hold_seconds) % 30 == 0:
logger.info(f"持仓中... 还需等待 {remaining:.0f}s")
return False, "", None
# ③ 满足持仓时间后的平仓检查
reverse = None
# 止损
if loss_pct >= self.stop_loss_pct:
return True, f"止损 (亏损 {loss_pct*100:.3f}%)", None
# 超时
if hold_seconds >= self.max_hold_seconds:
return True, f"超时平仓 (持仓 {hold_seconds:.0f}s)", None
# 反向交叉 → 平仓 + 可能反手
if self.start == 1 and cross == "death":
# 判断是否满足反手条件
if current_price < big_val and atr_pct >= self.atr_min_pct:
reverse = "short"
return True, "EMA死叉反转", reverse
if self.start == -1 and cross == "golden":
if current_price > big_val and atr_pct >= self.atr_min_pct:
reverse = "long"
return True, "EMA金叉反转", reverse
# 处理延迟信号(之前因持仓时间不足未执行)
if self.pending_signal == "close_long" and self.start == 1:
if fast_val < slow_val and current_price < big_val and atr_pct >= self.atr_min_pct:
reverse = "short"
self.pending_signal = None
return True, "延迟死叉平仓", reverse
if self.pending_signal == "close_short" and self.start == -1:
if fast_val > slow_val and current_price > big_val and atr_pct >= self.atr_min_pct:
reverse = "long"
self.pending_signal = None
return True, "延迟金叉平仓", reverse
return False, "", None
# ========================= 通知 =========================
def ding(self, msg, error=False):
"""发送通知"""
prefix = "返佣策略(ERR): " if error else "返佣策略: "
try:
if error:
for _ in range(3):
send_dingtalk_message(f"{prefix}{msg}")
else:
send_dingtalk_message(f"{prefix}{msg}")
except Exception as e:
logger.warning(f"通知发送失败: {e}")
def print_daily_stats(self):
"""打印当日统计"""
elapsed = time.time() - self.start_time
hours = elapsed / 3600
# 预估返佣90% 的手续费)
fee_rate = 0.0006 # taker 0.06%
total_fee = self.total_volume * fee_rate
rebate = total_fee * 0.9 # 90% 返佣
now = datetime.datetime.now().strftime("%H:%M:%S")
stats = (
f"\n{'='*50}\n"
f"[{now}] EMA趋势返佣策略统计\n"
f"运行时长: {hours:.1f} 小时\n"
f"交易次数: {self.trade_count}\n"
f"交易总额: {self.total_volume:.2f} USDT\n"
f"交易盈亏: {self.total_pnl:+.4f} USDT\n"
f"预估手续费: {total_fee:.4f} USDT\n"
f"预估返佣收入: {rebate:.4f} USDT\n"
f"预估净收益: {self.total_pnl + rebate:.4f} USDT\n"
f"{'='*50}"
)
logger.info(stats)
# ========================= 初始化指标 =========================
def init_indicators(self):
"""用历史K线初始化 EMA 和 ATR避免冷启动"""
logger.info("正在加载历史K线初始化指标...")
klines = self.get_1min_klines(count=150)
if not klines or len(klines) < self.ema_big_period:
logger.warning(f"历史K线不足 {self.ema_big_period} 根,指标将在运行中逐步初始化")
return False
# 除最后一根当前未完成的K线全部用于初始化
for kline in klines[:-1]:
self.process_new_kline(kline)
self.last_kline_time = kline['timestamp']
logger.info(
f"指标初始化完成 | {len(klines)-1} 根K线 | "
f"EMA8={self.ema_fast.value:.2f} EMA21={self.ema_slow.value:.2f} "
f"EMA120={self.ema_big.value:.2f}"
)
return True
# ========================= 主循环 =========================
def action(self):
"""主循环"""
# 设置杠杆
if not self.set_leverage():
logger.error("杠杆设置失败,退出")
self.ding("杠杆设置失败", error=True)
return
# 启动时获取持仓状态
if not self.get_position_status():
logger.warning("初始持仓状态获取失败,假设无仓位")
else:
if self.start != 0:
self.open_time = time.time() # 如果已有仓位,设置开仓时间为当前
logger.info(f"检测到已有持仓: {'' if self.start == 1 else ''}")
# 初始化技术指标
self.init_indicators()
logger.info(
f"EMA趋势返佣策略启动\n"
f" 交易对: {self.contract_symbol}\n"
f" 杠杆: {self.leverage}x 全仓\n"
f" EMA: 快{self.ema_fast_period} / 慢{self.ema_slow_period} / 大{self.ema_big_period}\n"
f" ATR过滤: > {self.atr_min_pct*100:.1f}% | 止损: {self.stop_loss_pct*100:.1f}%\n"
f" 最低持仓: {self.min_hold_seconds}s | 最长持仓: {self.max_hold_seconds}s\n"
f" 仓位比例: {self.risk_percent*100:.1f}%\n"
)
self.ding(
f"策略启动 | {self.contract_symbol} | {self.leverage}x\n"
f"EMA({self.ema_fast_period}/{self.ema_slow_period}/{self.ema_big_period}) "
f"ATR>{self.atr_min_pct*100:.1f}%"
)
stats_timer = time.time()
while True:
try:
# ① 获取最新 K 线
klines = self.get_1min_klines(count=5)
if not klines:
logger.warning("K线数据获取失败等待...")
time.sleep(self.check_interval)
continue
# ② 检查是否有新的完成K线需要处理
latest_completed = klines[-2] if len(klines) >= 2 else None
current_bar = klines[-1]
current_price = current_bar['close']
new_bar_processed = False
if latest_completed and latest_completed['timestamp'] != self.last_kline_time:
# 新K线完成处理信号
cross, atr_pct, fast_val, slow_val, big_val = self.process_new_kline(latest_completed)
self.last_kline_time = latest_completed['timestamp']
new_bar_processed = True
# ③ 有持仓 → 检查平仓
if self.start != 0:
should_close, reason, reverse = self.check_close_signal(
current_price, cross, atr_pct, fast_val, slow_val, big_val
)
if should_close:
self.close_position(reason, current_price)
# 反手开仓
if reverse:
time.sleep(1)
self.open_position(reverse, current_price)
time.sleep(1)
continue
# ④ 无持仓 → 检查开仓
if self.start == 0:
signal = self.check_open_signal(current_price, cross, atr_pct, big_val)
if signal:
self.open_position(signal, current_price)
time.sleep(1)
continue
# ⑤ 未收到新K线时仅检查止损/硬止损(实时保护)
if not new_bar_processed and self.start != 0 and self.open_avg_price:
if self.start == 1:
loss_pct = (self.open_avg_price - current_price) / self.open_avg_price
else:
loss_pct = (current_price - self.open_avg_price) / self.open_avg_price
# 硬止损实时检查
if loss_pct >= self.hard_stop_pct:
self.close_position(f"硬止损 (亏损 {loss_pct*100:.3f}%)", current_price)
time.sleep(1)
continue
# 满足持仓时间后的止损检查
if self.open_time and (time.time() - self.open_time) >= self.min_hold_seconds:
if loss_pct >= self.stop_loss_pct:
self.close_position(f"止损 (亏损 {loss_pct*100:.3f}%)", current_price)
time.sleep(1)
continue
# 超时检查
hold_sec = time.time() - self.open_time
if hold_sec >= self.max_hold_seconds:
self.close_position(f"超时平仓 ({hold_sec:.0f}s)", current_price)
time.sleep(1)
continue
# 延迟信号处理
if self.pending_signal and self.open_time and \
(time.time() - self.open_time) >= self.min_hold_seconds:
atr_pct = self.calculate_atr_pct(current_price)
fast_val = self.ema_fast.value
slow_val = self.ema_slow.value
big_val = self.ema_big.value
should_close, reason, reverse = self.check_close_signal(
current_price, None, atr_pct, fast_val, slow_val, big_val
)
if should_close:
self.close_position(reason, current_price)
if reverse:
time.sleep(1)
self.open_position(reverse, current_price)
time.sleep(1)
continue
# ⑥ 定期打印统计(每 10 分钟)
if time.time() - stats_timer >= 600:
self.print_daily_stats()
stats_timer = time.time()
# ⑦ 每轮日志
hold_info = ""
if self.start != 0 and self.open_time:
hold_seconds = time.time() - self.open_time
direction = "" if self.start == 1 else ""
if self.open_avg_price:
if self.start == 1:
pnl_pct = (current_price - self.open_avg_price) / self.open_avg_price * 100
else:
pnl_pct = (self.open_avg_price - current_price) / self.open_avg_price * 100
hold_info = f" | 持{direction} {hold_seconds:.0f}s PnL:{pnl_pct:+.3f}%"
if self.ema_fast.value and self.ema_slow.value and self.ema_big.value:
logger.debug(
f"价格 {current_price:.2f} | "
f"EMA [{self.ema_fast.value:.2f}/{self.ema_slow.value:.2f}/{self.ema_big.value:.2f}] | "
f"ATR {self.calculate_atr_pct(current_price)*100:.3f}%{hold_info}"
)
time.sleep(self.check_interval)
except KeyboardInterrupt:
logger.info("用户中断")
self.print_daily_stats()
# 中断时如果有仓位,提示手动处理
if self.start != 0:
logger.warning("当前仍有持仓,请手动处理!")
break
except Exception as e:
logger.error(f"主循环异常: {e}")
time.sleep(10)
if __name__ == '__main__':
BitmartRebateStrategy().action()