完整示例
ETF 申赎与两融示例
两份可直接运行的回测策略:ETF 一级申赎篮子策略(准备成分券后申购再赎回、买入 ETF 后赎回、只替代指定成分券),以及证券两融往返策略(融资或融券,交易偿还或直接偿还)。
两份策略都比二级买卖复杂:要处理跨日交收、实际清算资料和信用负债。运行前先读 ETF 申购与赎回和融资融券。复制整段即可作为一份独立策略运行,示例用于演示 API 用法,不承诺收益。
一、ETF 一级申赎篮子策略
一份代码覆盖三种用法,用参数切换:
| 参数 | 默认值 | 用法 |
|---|---|---|
etf_symbol |
'510050.SH' |
本轮申赎的 ETF |
basket_count |
1 |
目标篮数,不是 ETF 份额 |
start_mode |
'先申购再赎回' |
或 '买入ETF后赎回' |
cash_buffer |
100000.0 |
补券时至少预留的现金,不保证覆盖所有产品的现金替代金额 |
cash_substitution |
False |
是否主动选择允许替代的成分券 |
cash_symbols |
[] |
非空时只替代列出的成分券,优先于上面的布尔选择 |
先申购再赎回(默认)。使用空仓、有足够整篮资金的账户。每天按当日清单补齐实物券;为满足买入数量要求多买的余股会留在账户里。本例保守地等待补券跨日可用后再申购,不演示日内最快的套利路径。申购金额按当日清单参考净值换篮,并核对返回的 units;申请等待期间不重复提交。申购全部交收后,按申请日最小单位重新检查赎回数量,再申请赎回。赎回完成后保留成分券和现金,停止新申请。
买入 ETF 后赎回。把 start_mode 改为 '买入ETF后赎回'。策略先在二级市场买入 最小申赎单位 × basket_count 份 ETF,达到可用数量后才提交正份额的 redeem。初始资金要按 ETF 二级价格、整篮份额和费用准备,不是每次只买 100 份。
只替代指定成分券。设置 cash_symbols,例如 ['600000.SH']。列表中只能填当日清单允许替代的证券;其余允许替代项用实物,必须替代、退补替代项仍按清单执行。全周期回测中,代码不在清单里或变为禁止替代时,示例会明确报错,不会悄悄换一种交付方式。
如果实际交收资料尚不可用,阶段会停在「待交收」;如果当日最小单位改变、数量不再是整篮,阶段会停在「待赎回」。这两种状态都不由策略强制结束。
"""单只股票ETF:准备实物成分券后申购赎回,或买入ETF后赎回。
从空仓账户启动,运行一轮。现金及成分券均使用任务的同一个证券账户。
日线1d、分钟1M均可;初始资金需覆盖整篮资产、现金替代和相关费用。
示例处理沪深股票实物篮子,不处理境外或虚拟现金项目。
赎回后保留到账成分券以及买入取整产生的余股,不自动卖出。
实际交收数据未接通时保留待交收;不会自行填充清算结果。
"""
from datetime import datetime, timedelta
from math import ceil, floor, isfinite
from pandas import isna
import panda_data
from panda_backtest.api.api import *
STRATEGY_PARAMS = {
'etf_symbol': '510050.SH',
'basket_count': 1,
'cash_buffer': 100000.0,
'cash_substitution': False,
'cash_symbols': [],
'start_mode': '先申购再赎回',
}
def initialize(context):
context.account = context.run_info.stock_account
context.etf = STRATEGY_PARAMS['etf_symbol']
context.baskets = STRATEGY_PARAMS['basket_count']
context.cash_buffer = float(STRATEGY_PARAMS['cash_buffer'])
context.cash_substitution = STRATEGY_PARAMS['cash_substitution']
if type(context.baskets) is not int or context.baskets < 1:
raise ValueError('申购篮数必须是正整数')
if type(context.cash_substitution) is not bool:
raise ValueError('本示例cash_substitution只接受True或False')
cash_symbols = list(STRATEGY_PARAMS['cash_symbols'])
if cash_symbols:
# 指定代码列表时,仅主动替代这些成分券;必须替代项仍按清单执行。
context.cash_substitution = {symbol: True for symbol in cash_symbols}
if not isfinite(context.cash_buffer) or context.cash_buffer < 0:
raise ValueError('现金预留必须是有限非负数')
if len(context.etf) != 9 or context.etf[-3:] not in ('.SH', '.SZ'):
raise ValueError('ETF请填写完整的沪深证券代码')
account = _account(context)
for position in account.positions.values():
if position.quantity > 0:
raise ValueError('一级篮子示例需要从空仓账户启动')
mode = STRATEGY_PARAMS['start_mode']
if mode not in ('先申购再赎回', '买入ETF后赎回'):
raise ValueError('start_mode请使用先申购再赎回或买入ETF后赎回')
context.phase = '准备成分券' if mode == '先申购再赎回' else '买入ETF'
context.current_date = None
context.attempted = set()
context.wait_messages = set()
context.pending = {}
context.finished = set()
context.seen_trades = set()
context.component_symbols = set()
context.last_buy_date = None
context.primary_action = None
context.primary_key = None
context.primary_finished = set()
context.primary_attempt_date = None
context.created_units = 0
context.created_date = None
# 在初始化阶段加载全周期,分段请求以满足接口单次查询范围限制。
# 沿用任务的数据认证,不调用init_token。
context.pcf_by_date = {}
context.limits_by_date = {}
context.components_by_date = {}
start = datetime.strptime(str(context.run_info.start_date), '%Y%m%d')
end = datetime.strptime(str(context.run_info.end_date), '%Y%m%d')
if start > end:
raise ValueError('回测开始日期不能晚于结束日期')
while start <= end:
chunk_end = min(start + timedelta(days=179), end)
params = {
'symbol': context.etf, 'exchange': context.etf[-2:],
'start_date': start.strftime('%Y%m%d'),
'end_date': chunk_end.strftime('%Y%m%d'),
}
_merge(context.pcf_by_date, panda_data.get_fund_etf_cr(**params), context.etf, params)
_merge(context.limits_by_date, panda_data.get_fund_etf_cr_limits(**params), context.etf, params)
_merge(context.components_by_date, panda_data.get_fund_etf_constituents(**params), context.etf, params)
start = chunk_end + timedelta(days=1)
if not context.pcf_by_date or not context.limits_by_date or not context.components_by_date:
raise ValueError('回测区间内的PCF、申赎额度或成分券数据为空')
print(f'[初始化] 账户={context.account},ETF={context.etf},篮数={context.baskets}')
def _account(context):
account = context.stock_account_dict.get(context.account)
if account is None:
raise ValueError(f'证券账户不存在:{context.account}')
return account
def _value(row, name):
value = row.get(name)
return None if value is None or bool(isna(value)) else value
def _positive(value, name):
if value is None or isinstance(value, bool):
raise ValueError(f'{name}缺失或无效')
number = float(value)
if not isfinite(number) or number <= 0:
raise ValueError(f'{name}必须是有限正数')
return number
def _integer(value, name):
number = _positive(value, name)
if not number.is_integer():
raise ValueError(f'{name}必须是整数')
return int(number)
def _group(frame, symbol):
grouped = {}
if frame is None or frame.empty:
return grouped
for row in frame.to_dict('records'):
if str(row.get('symbol')) != symbol:
raise ValueError('业务数据返回了不匹配的ETF代码')
date = str(row.get('date', '')).replace('-', '')[:8]
if len(date) != 8 or not date.isdigit():
raise ValueError('业务数据交易日期无效')
grouped.setdefault(date, []).append(row)
return grouped
def _merge(target, frame, symbol, params):
if frame is None:
raise ValueError('业务数据接口没有返回结果,不能视为已确认的空数据')
for date, rows in _group(frame, symbol).items():
if not params['start_date'] <= date <= params['end_date']:
raise ValueError('业务数据返回了请求范围以外的日期')
target.setdefault(date, []).extend(rows)
def _wait(context, reason):
if reason not in context.wait_messages:
context.wait_messages.add(reason)
print(f'[{context.trade_date} {context.hms}] [等待] {reason}')
def _snapshot(context):
today = str(context.trade_date)
rules = context.pcf_by_date.get(today, [])
limits = context.limits_by_date.get(today, [])
components = context.components_by_date.get(today, [])
if not rules or not limits or not components:
_wait(context, '当日PCF、额度或成分券数据不齐')
return None
if len(rules) != 1 or len(limits) != 1:
raise ValueError('同一交易日的PCF或额度数据存在重复行')
rule, limit = rules[0], limits[0]
rule_unit = _value(rule, 'unit')
if rule_unit is None:
rule_unit = _value(rule, 'min_cr_unit')
limit_unit = _value(limit, 'min_cr_unit')
if limit_unit is None:
limit_unit = _value(limit, 'unit')
if rule_unit is not None:
rule_unit = _integer(rule_unit, 'PCF最小申赎单位')
if limit_unit is not None:
limit_unit = _integer(limit_unit, '额度表最小申赎单位')
if rule_unit is not None and limit_unit is not None and rule_unit != limit_unit:
raise ValueError('当日PCF和额度表的最小申赎单位不一致')
unit = limit_unit if limit_unit is not None else rule_unit
unit = _integer(unit, '最小申赎单位')
nav = _value(rule, 'unit_nav')
if nav is None:
nav = _value(limit, 'nav')
if nav is None:
nav = _positive(_value(rule, 'creation_unit'), '整篮参考资产净值') / unit
nav = _positive(nav, 'PCF参考单位净值')
return rule, unit, nav, components
def _physical_targets(context, rows):
targets, seen = {}, set()
if isinstance(context.cash_substitution, dict):
names = {str(_value(row, 'stock_symbol') or '') for row in rows}
if set(context.cash_substitution) - names:
raise ValueError('现金替代选择包含当日清单以外的证券')
for row in rows:
symbol = str(_value(row, 'stock_symbol') or '')
if len(symbol) != 9 or not symbol[:6].isdigit() or symbol[-3:] not in ('.SH', '.SZ'):
raise ValueError(f'本示例仅准备沪深股票实物篮子:{symbol}')
if symbol in seen:
raise ValueError(f'成分券重复:{symbol}')
seen.add(symbol)
flag = _integer(_value(row, 'cash_substitution_flag'), '现金替代标志')
if flag not in (1, 2, 3, 4):
raise ValueError(f'{symbol} 现金替代标志不支持:{flag}')
# 必须现金替代、退补现金替代仍走现金,不受False影响。
selected = (context.cash_substitution.get(symbol, False)
if isinstance(context.cash_substitution, dict)
else context.cash_substitution)
if isinstance(context.cash_substitution, dict) and symbol in context.cash_substitution and flag == 3:
raise ValueError(f'{symbol} 禁止现金替代,请从cash_symbols中移除')
cash = flag in (2, 4) or (flag == 1 and selected)
if not cash:
targets[symbol] = _integer(_value(row, 'quantity'), '成分券数量') * context.baskets
context.component_symbols.update(seen)
return targets
def _price(context, data, symbol):
try:
bar = data[symbol]
except KeyError:
return None
if bar is None:
return None
value = bar.open if context.run_info.matching_type == 1 else bar.close
try:
price = float(value)
except (TypeError, ValueError):
return None
return price if isfinite(price) and price > 0 else None
def _track_stock(context, order):
if order.account != context.account or order.order_book_id not in context.component_symbols:
return
key = (order.account, order.order_id)
if order.status in (-1, 2, 3, 5, 7):
context.finished.add(key)
context.pending.pop(key, None)
elif key not in context.finished:
context.pending[key] = order.order_book_id
def _prepare(context, data, targets):
ready = True
for symbol, required in targets.items():
account = _account(context)
position = account.positions.get(symbol)
held = 0 if position is None else int(position.quantity)
if held >= required:
if position.sellable < required:
ready = False
_wait(context, f'{symbol} 尚未具备本示例要求的可用数量')
continue
ready = False
if symbol in context.attempted or symbol in context.pending.values():
continue
price = _price(context, data, symbol)
if price is None:
_wait(context, f'{symbol} 当前无有效价格')
continue
# 向上买整手补齐篮子,多出的证券留在账户中。
quantity = int(ceil((required - held) / 100)) * 100
if symbol.startswith(('688', '689')):
quantity = max(200, quantity)
if account.cash < quantity * price * 1.02 + context.cash_buffer:
_wait(context, f'{symbol} 补券资金不足,需预留现金替代和费用')
continue
context.attempted.add(symbol)
context.last_buy_date = str(context.trade_date)
orders = order_shares(
context.account, symbol, quantity, style=MarketOrderStyle(),
remark=f'ETF篮子补券_{context.trade_date}',
)
if not orders:
print(f'[{context.trade_date}] {symbol} 补券未返回委托')
for order in orders or []:
_track_stock(context, order)
key = (order.account, order.order_id)
if order.status in (1, 4) and key in context.pending:
cancel_order(context.account, order.order_id)
return ready and not context.pending and context.last_buy_date != str(context.trade_date)
def _track_primary(context, order):
if order.account != context.account or order.symbol != context.etf:
return
key = (order.account, 'PRIMARY', str(order.order_id))
if key in context.primary_finished:
return
if context.primary_action is None:
return
if context.primary_key is not None and context.primary_key != key:
return
context.primary_key = key
print(
f'[申赎回报] 操作={context.primary_action},申请={order.order_id},'
f'份额={order.units},状态={order.status},阶段={order.stage},说明={order.reason}'
)
if order.status in (-1, 3):
context.primary_finished.add(key)
context.phase = '准备成分券' if context.primary_action == '申购' else '待赎回'
context.primary_action = None
context.primary_key = None
elif order.status == 2 and order.stage == 'SETTLED':
context.primary_finished.add(key)
if context.primary_action == '申购':
context.created_units = int(order.units)
context.created_date = str(context.trade_date)
context.phase = '待赎回'
else:
context.phase = '完成'
context.primary_action = None
context.primary_key = None
def handle_data(context, data):
today = str(context.trade_date)
if context.current_date != today:
context.current_date = today
context.attempted = set()
context.wait_messages = set()
if context.phase == '完成':
return
if context.phase == '待交收':
_wait(context, '已申报,等待实际确认及交收,不重复提交')
return
snapshot = _snapshot(context)
if snapshot is None:
return
rule, unit, nav, rows = snapshot
if context.phase == '买入ETF':
context.component_symbols.add(context.etf)
required = unit * context.baskets
if _prepare(context, data, {context.etf: required}):
context.created_units = required
context.phase = '待赎回'
return
if context.phase == '准备成分券':
if _value(rule, 'purchase_allowed_flag') not in (1, '1', True, 'Y'):
_wait(context, '当日PCF未允许申购')
return
targets = _physical_targets(context, rows)
if not _prepare(context, data, targets):
return
if context.primary_attempt_date == today:
return
# purchase收金额,不收份额;按当日PCF参考净值换成整篮。
expected_units = unit * context.baskets
amount = nav * unit * (context.baskets + 0.00000001)
if not isfinite(amount) or floor(amount / nav / unit) != context.baskets:
raise ValueError('申购金额无法准确换算为目标篮数')
context.primary_attempt_date = today
context.primary_action = '申购'
context.primary_key = None
context.phase = '待交收'
order = purchase(
context.account, context.etf, amount,
cash_substitution=context.cash_substitution,
remark=f'整篮申购_{today}',
)
if order is None:
raise RuntimeError('申购未返回申请,需核对结果,停止以避免重复提交')
_track_primary(context, order)
if order.status != -1 and int(order.units) != expected_units:
raise RuntimeError('已受理份额与目标不一致,停止后续下单,请核对PCF口径')
elif context.phase == '待赎回':
if today == context.created_date or context.primary_attempt_date == today:
return
if _value(rule, 'redemption_allowed_flag') not in (1, '1', True, 'Y'):
_wait(context, '当日PCF未允许赎回')
return
if context.created_units <= 0 or context.created_units % unit != 0:
_wait(context, '申购到账份额不满足当前最小赎回单位,不擅自取整')
return
position = _account(context).positions.get(context.etf)
if position is None or position.sellable < context.created_units:
_wait(context, '申购到账ETF尚未具备本示例要求的可用数量')
return
context.primary_attempt_date = today
context.primary_action = '赎回'
context.primary_key = None
context.phase = '待交收'
order = redeem(
context.account, context.etf, context.created_units,
cash_substitution=False, remark=f'整篮赎回_{today}',
)
if order is None:
raise RuntimeError('赎回未返回申请,需核对结果,停止以避免重复提交')
_track_primary(context, order)
def on_etf_cr_order_rtn(context, order):
_track_primary(context, order)
def on_etf_cr_confirm(context, result):
_track_primary(context, result)
def etf_cr_order_cancel(context, order):
_track_primary(context, order)
def on_stock_order_rtn(context, order):
_track_stock(context, order)
if order.account == context.account and order.order_book_id in context.component_symbols:
print(
f'[补券订单回报] {order.order_book_id},委托={order.order_id},'
f'状态={order.status},已成={order.filled_quantity},说明={order.message}'
)
def on_stock_trade_rtn(context, trade):
if trade.account_id != context.account or trade.contract_code not in context.component_symbols:
return
key = (trade.account_id, trade.trade_date, trade.order_id, trade.trade_id)
if key in context.seen_trades:
return
context.seen_trades.add(key)
print(
f'[补券成交回报] {trade.contract_code},数量={trade.volume},'
f'价格={trade.price},费用={trade.cost}'
)
def stock_order_cancel(context, order):
_track_stock(context, order)
if order.account == context.account and order.order_book_id in context.component_symbols:
print(f'[补券撤单或拒单] {order.order_id},状态={order.status},说明={order.message}')
def after_trading(context):
account = _account(context)
print(
f'[{context.trade_date}] 阶段={context.phase},现金={account.cash:.2f},'
f'冻结现金={account.frozen_cash:.2f},净资产={account.total_value:.2f}'
)
for symbol, position in account.positions.items():
if position.quantity > 0:
print(f'[持仓] {symbol} 数量={position.quantity},可卖={position.sellable}')
二、证券两融往返策略
一轮借入、一轮偿还,股票与 ETF 写法相同。
| 参数 | 默认值 | 可选设置 |
|---|---|---|
symbol |
'510300.SH' |
股票或 ETF 完整代码 |
quantity |
1000 |
正的 100 股/份整数倍 |
credit_mode |
'融资' |
'融资'、'融券' |
repay_method |
'交易偿还' |
'交易偿还'、'直接偿还' |
hold_trading_days |
2 |
首次形成负债后持有的交易日数 |
四种组合对应的操作:
| 场景 | 借入 | 偿还 |
|---|---|---|
| 融资 + 交易偿还 | financing_buy |
sell_repayment;卖出后仍有现金负债时按可用现金继续还款 |
| 融资 + 直接偿还 | financing_buy |
repay_financing,还完后保留买入的现券 |
| 融券 + 交易偿还 | securities_lending_sell |
buy_to_cover |
| 融券 + 直接偿还 | securities_lending_sell |
普通买入现券,跨日后 return_securities |
运行前在任务中启用两融,设置融资额度、标的范围和所需券源;账户从空仓、无信用负债开始,一轮后停止。用股票运行时,把 symbol 改为如 600000.SH、quantity 改为 100,并在任务中为它配好额度、名单和券源。直接还券需要先有可交付现券,而现金买回的股票当天仍可能受 T+1 限制。
示例按实际形成的债务安排偿还,不按最初申请数量假定全部成交。发生强平时停止追加普通订单,继续观察回报和负债。只有最新信用快照的 total_debt 已清偿,才显示本轮完成;剩余债务不会因为回测临近结束而被清零。偿还后要同时检查本金、利息、欠券和普通持仓,不能只看其中一个余额。
"""证券两融示例,一轮借入和偿还;股票与ETF共用同一写法。
任务中先启用两融,配置融资额度、可交易标的及融券券源。
credit_mode:融资/融券;repay_method:交易偿还/直接偿还。
融资交易偿还使用卖券还款;融券交易偿还使用买券还券。
直接偿还模式:融资用自有现金还款;融券先买入现券,跨日后直接还券。
从空仓、无信用负债的账户启动。日线或分钟均可,不修改账户配置。
"""
from math import ceil, isfinite
from panda_backtest.api.api import *
STRATEGY_PARAMS = {
'symbol': '510300.SH',
'quantity': 1000,
'credit_mode': '融资',
'repay_method': '交易偿还',
'hold_trading_days': 2,
}
def initialize(context):
context.account = context.run_info.stock_account
context.symbol = STRATEGY_PARAMS['symbol']
context.quantity = STRATEGY_PARAMS['quantity']
context.mode = STRATEGY_PARAMS['credit_mode']
context.method = STRATEGY_PARAMS['repay_method']
context.hold_days = STRATEGY_PARAMS['hold_trading_days']
if context.mode not in ('融资', '融券') or context.method not in ('交易偿还', '直接偿还'):
raise ValueError('信用方向或偿还方式无效')
if type(context.quantity) is not int or context.quantity <= 0 or context.quantity % 100:
raise ValueError('本示例数量使用正的100整数倍')
if type(context.hold_days) is not int or context.hold_days < 1:
raise ValueError('持有交易日数必须是正整数')
context.phase = '借入'
context.date = None
context.day_index = 0
context.hold_start = None
context.attempt_date = None
context.stock_buy_date = None
context.pending = {}
context.finished = set()
context.trades = set()
account = _account(context)
if any(position.quantity > 0 for position in account.positions.values()):
raise ValueError('本示例从空仓账户启动')
snapshot = _credit(context)
if snapshot['total_debt'] > 0:
raise ValueError('本示例需要账户没有已有信用负债')
if context.mode == '融资' and context.run_info.margin_credit_limit <= 0:
raise ValueError('请在回测任务中设置正的融资额度')
def _account(context):
account = context.stock_account_dict.get(context.account)
if account is None:
raise ValueError('证券账户不存在')
return account
def _credit(context):
snapshot = get_margin_snapshot(context.account)
if not snapshot or not snapshot.get('enabled'):
raise ValueError('请先在任务设置中启用证券两融')
return snapshot
def _loan_quantity(context, snapshot):
record = snapshot['lending'].get(context.symbol)
return 0 if record is None else int(record['quantity'])
def _track(context, order):
if order.account != context.account or order.order_book_id != context.symbol:
return
key = (order.account, order.order_id)
if order.status in (-1, 2, 3, 5, 7):
context.finished.add(key)
context.pending.pop(key, None)
elif key not in context.finished:
context.pending[key] = order.order_id
def _accept_orders(context, orders):
for order in orders or []:
_track(context, order)
if order.status in (1, 4, 6) and (order.account, order.order_id) in context.pending:
cancel_order(context.account, order.order_id)
def _price(context, data):
try:
bar = data[context.symbol]
except KeyError:
return None
if bar is None:
return None
value = bar.open if context.run_info.matching_type == 1 else bar.close
if value is None:
return None
price = float(value)
return price if isfinite(price) and price > 0 else None
def handle_data(context, data):
today = str(context.trade_date)
if today != context.date:
context.date = today
context.day_index += 1
if context.phase == '完成':
return
snapshot = _credit(context)
if snapshot['risk_state'] == 'LIQUIDATION':
# 不与自动强平同时追加买卖,后续继续观察真实负债。
context.phase = '偿还'
return
if context.pending:
return
if context.phase == '借入':
borrowed = (snapshot['financing_balance'] > 0 if context.mode == '融资'
else _loan_quantity(context, snapshot) > 0)
if borrowed:
context.phase = '持有'
context.hold_start = context.day_index
elif context.attempt_date != today and snapshot['risk_state'] == 'NORMAL':
if _price(context, data) is None:
return
context.attempt_date = today
if context.mode == '融资':
orders = financing_buy(context.account, context.symbol, context.quantity,
style=MarketOrderStyle(), remark='融资示例')
else:
orders = securities_lending_sell(context.account, context.symbol, context.quantity,
style=MarketOrderStyle(), remark='融券示例')
_accept_orders(context, orders)
snapshot = _credit(context)
borrowed = (snapshot['financing_balance'] > 0 if context.mode == '融资'
else _loan_quantity(context, snapshot) > 0)
if borrowed:
context.phase = '持有'
context.hold_start = context.day_index
return
else:
return
if context.phase == '持有':
if context.day_index - context.hold_start < context.hold_days:
return
context.phase = '偿还'
if snapshot['total_debt'] <= 0:
context.phase = '完成'
print(f'[{today}] 信用负债已清偿;现券持仓保持不动')
return
if context.attempt_date == today:
return
account = _account(context)
position = account.positions.get(context.symbol)
sellable = 0 if position is None else int(position.sellable)
context.attempt_date = today
if context.mode == '融资':
debt = float(snapshot['financing_balance']) + float(snapshot['financing_interest'])
if context.method == '交易偿还' and sellable > 0:
if _price(context, data) is None:
context.attempt_date = None
return
_accept_orders(context, sell_repayment(
context.account, context.symbol, sellable,
style=MarketOrderStyle(), remark='卖券还款示例',
))
elif context.method == '直接偿还' or position is None or position.quantity <= 0:
amount = min(float(account.cash), debt)
if amount > 0:
actual = repay_financing(context.account, amount)
print(f'[直接还款] 实际归还={actual}')
else:
quantity = _loan_quantity(context, snapshot)
if quantity > 0 and context.method == '交易偿还':
if _price(context, data) is None:
context.attempt_date = None
return
_accept_orders(context, buy_to_cover(
context.account, context.symbol, quantity,
style=MarketOrderStyle(), remark='买券还券示例',
))
elif quantity > 0:
if sellable >= quantity and context.stock_buy_date != today:
actual = return_securities(context.account, context.symbol, quantity)
print(f'[直接还券] 实际归还={actual}')
else:
held = 0 if position is None else int(position.quantity)
missing = quantity - held
price = _price(context, data)
if missing > 0 and price is not None:
buy_quantity = int(ceil(missing / 100)) * 100
if context.symbol.startswith(('688', '689')):
buy_quantity = max(200, buy_quantity)
if account.cash >= buy_quantity * price * 1.02 + 100:
context.stock_buy_date = today
_accept_orders(context, order_shares(
context.account, context.symbol, buy_quantity,
style=MarketOrderStyle(), remark='为直接还券买入现券',
))
if not context.pending and _credit(context)['total_debt'] <= 0:
context.phase = '完成'
print(f'[{today}] 信用负债已清偿;现券持仓保持不动')
def on_stock_order_rtn(context, order):
_track(context, order)
if order.account == context.account and order.order_book_id == context.symbol:
print(f'[订单] {order.order_id} 状态={order.status} 用途={order.margin_usage} '
f'强平={order.force_liquidation} 说明={order.message}')
def on_stock_trade_rtn(context, trade):
if trade.account_id != context.account or trade.contract_code != context.symbol:
return
key = (trade.account_id, trade.trade_date, trade.order_id, trade.trade_id)
if key in context.trades:
return
context.trades.add(key)
print(f'[成交] {trade.contract_code} 数量={trade.volume} 价格={trade.price} '
f'用途={trade.margin_usage} 强平={trade.force_liquidation} 费用={trade.cost}')
def stock_order_cancel(context, order):
_track(context, order)
if order.account == context.account and order.order_book_id == context.symbol:
print(f'[撤单或拒单] {order.order_id} 状态={order.status} 说明={order.message}')
def after_trading(context):
snapshot = _credit(context)
print(f'[{context.trade_date}] 阶段={context.phase},总负债={snapshot["total_debt"]},'
f'融资={snapshot["financing_balance"]},融券数量={_loan_quantity(context, snapshot)},'
f'风险状态={snapshot["risk_state"]}')
