Coin Flipping 策略
概述
Coin Flipping 策略是对经典 MetaTrader 智能交易程序的直接移植,它通过“掷硬币”来决定买入还是卖出。每当上一根K线完成并且策略没有持仓时,就会触发一次新的决定,因此交易形成连续且相互独立的序列。StockSharp 版本刻意保持这种极简行为:同时只持有一笔仓位,并且为每笔交易设置对称的止盈和止损距离(以点数表示)。
尽管逻辑非常天真,这个示例展示了如何把体量很小的 EA 转换到 StockSharp 的高级 API 中。它适合作为示范,说明如何配置数据订阅、资金管理辅助函数以及保护性订单。
交易逻辑
- 策略启动时使用当前系统计时器为随机数生成器设定种子,对应原始 MQL 代码中的
MathSrand(GetTickCount())。 - 对于每一根收盘完成的K线(默认周期为1分钟,可替换为任何蜡烛类型),策略会检查是否允许交易并确认当前没有持仓。
- 在空仓状态下,随机数生成器产生 0 或 1。结果为 0 时发送市价买单,结果为 1 时发送市价卖单。下单数量根据设定的风险百分比和止损距离动态计算。
- 通过
StartProtection创建的保护性订单会为每笔仓位自动附加止损与止盈,从而无需手动管理退出。
没有其他过滤条件:一旦上一笔交易平仓,下一根K线立即重新“掷硬币”。
仓位规模
在 StockSharp 版本中,仓位规模公式被改写为适应组合资产。风险金额的计算方式为 Portfolio.CurrentValue * RiskPercent / 100,即按组合市值的一定比例承担风险。该金额除以换算成价格单位的止损距离(使用标的的最小价格跳动转换点数)即可得到下单数量。随后,帮助函数会按照 VolumeStep 的粒度对数量进行取整,并遵守标的的 MinVolume 与 MaxVolume 限制。
这样既保留了原代码“每笔交易冒固定比例资金”的思路,又保证委托数量符合 StockSharp 对证券的约束。
参数
| 参数 | 说明 | 默认值 | 备注 |
|---|---|---|---|
RiskPercent |
每笔交易投入的组合资金百分比。 | 2 |
数值越大下单量越大,反之越小。 |
TakeProfitPips |
入场价到止盈位的距离(点)。 | 20 |
通过价格步长转换为绝对价格并传递给 StartProtection。 |
StopLossPips |
入场价到止损位的距离(点)。 | 10 |
同样会转换为价格单位,并用于仓位规模计算。 |
CandleType |
用于驱动决策循环的蜡烛订阅。 | 1 分钟周期 |
可替换为任意 StockSharp 蜡烛类型;周期越大交易频率越低。 |
风险管理
StartProtection 在 OnStarted 中被调用一次,使用计算好的止盈和止损距离。之后由 StockSharp 自动维护保护性订单,模拟了 MQL OrderSend 函数中止盈止损参数的效果。由于策略只在 Position == 0 时才开仓,无需手动撤单或重建保护性订单;仓位平仓后平台会自动取消它们。
实现细节
- 使用高级
SubscribeCandles().Bind(...)模式处理K线,使代码简洁且易读。 - 日志记录会输出方向和下单数量,便于回测时观察伪随机生成器的表现。
- 仓位规模调整会考虑
VolumeStep、MinVolume、MaxVolume,确保所有订单满足交易所规则。 - 按仓库要求,代码中的注释全部使用英文,并遵循既定结构。
使用建议
- 由于方向完全随机,该策略并不追求长期盈利,更多用于演示或测试基础设施。
- 请确保绑定的组合
CurrentValue为正值,否则风险计算结果为零,将不会下单。 - 如果想降低或提高“掷硬币”的频率,可调整蜡烛类型(例如改成小时级别或逐笔数据)。
- 在优化时可以尝试不同的止盈止损距离,或降低风险百分比,以控制潜在回撤。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Randomized coin flipping strategy that alternates between buying and selling based on a pseudo-random generator.
/// Mimics the original MetaTrader expert advisor by opening a single position at a time with symmetric risk controls.
/// </summary>
public class CoinFlippingStrategy : Strategy
{
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<DataType> _candleType;
private Random _random;
private decimal _priceStep;
private decimal _takeProfitDistance;
private decimal _stopLossDistance;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
/// <summary>
/// Portfolio share allocated to every trade in percent.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Take profit distance measured in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Stop loss distance measured in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Candle type used for scheduling trade attempts.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize strategy parameters.
/// </summary>
public CoinFlippingStrategy()
{
_riskPercent = Param(nameof(RiskPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Portfolio percentage allocated per trade", "Risk Management")
.SetOptimize(1m, 10m, 1m);
_takeProfitPips = Param(nameof(TakeProfitPips), 5000)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Target distance expressed in pips", "Risk Management")
.SetOptimize(10, 50, 5);
_stopLossPips = Param(nameof(StopLossPips), 3000)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk Management")
.SetOptimize(5, 30, 5);
_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for trade timing", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Reset cached state when the strategy is reset.
_random = null;
_priceStep = 0m;
_takeProfitDistance = 0m;
_stopLossDistance = 0m;
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Seed the pseudo-random generator similarly to the MQL expert.
_random = new Random(System.Environment.TickCount);
// Determine price step information for translating pips into price units.
_priceStep = Security?.PriceStep ?? 1m;
if (_priceStep <= 0m)
_priceStep = 1m;
_takeProfitDistance = TakeProfitPips * _priceStep;
_stopLossDistance = StopLossPips * _priceStep;
// Subscribe to candle data to trigger decision making once per bar.
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
// Only use completed candles to avoid duplicate executions while a bar is forming.
if (candle.State != CandleStates.Finished)
return;
// Check risk management first.
if (Position > 0)
{
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Position);
ResetTargets();
}
else if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(Position);
ResetTargets();
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetTargets();
}
else if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetTargets();
}
}
// The strategy maintains at most one position at a time.
if (Position != 0)
return;
if (_random == null)
return;
var entryPrice = candle.ClosePrice;
if (entryPrice <= 0m)
return;
var volume = CalculateOrderVolume(entryPrice);
if (volume <= 0m)
return;
var isBuy = _random.Next(0, 2) == 0;
if (isBuy)
{
BuyMarket(volume);
_entryPrice = entryPrice;
_stopPrice = _stopLossDistance > 0m ? entryPrice - _stopLossDistance : null;
_takePrice = _takeProfitDistance > 0m ? entryPrice + _takeProfitDistance : null;
}
else
{
SellMarket(volume);
_entryPrice = entryPrice;
_stopPrice = _stopLossDistance > 0m ? entryPrice + _stopLossDistance : null;
_takePrice = _takeProfitDistance > 0m ? entryPrice - _takeProfitDistance : null;
}
}
private decimal CalculateOrderVolume(decimal entryPrice)
{
var balance = Portfolio?.CurrentValue ?? 0m;
if (balance <= 0m)
return 0m;
var riskAmount = balance * RiskPercent / 100m;
if (riskAmount <= 0m)
return 0m;
var stopDistance = _stopLossDistance;
if (stopDistance <= 0m)
{
stopDistance = StopLossPips * _priceStep;
}
if (stopDistance <= 0m)
return 0m;
// Risk per unit equals the stop distance; divide to get the number of contracts.
var rawVolume = riskAmount / stopDistance;
var volume = NormalizeVolume(rawVolume);
if (volume <= 0m)
{
volume = Volume > 0m ? Volume : 1m;
volume = NormalizeVolume(volume);
}
return volume;
}
private decimal NormalizeVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var step = Security?.VolumeStep;
if (step.HasValue && step.Value > 0m)
{
volume = Math.Floor(volume / step.Value) * step.Value;
}
return volume > 0m ? volume : 1m;
}
private void ResetTargets()
{
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
}
}
import clr
import random
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class coin_flipping_strategy(Strategy):
def __init__(self):
super(coin_flipping_strategy, self).__init__()
self._risk_percent = self.Param("RiskPercent", 2.0)
self._take_profit_pips = self.Param("TakeProfitPips", 5000)
self._stop_loss_pips = self.Param("StopLossPips", 3000)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromDays(1)))
self._rng = None
self._price_step = 1.0
self._tp_dist = 0.0
self._sl_dist = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RiskPercent(self):
return self._risk_percent.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
def OnStarted2(self, time):
super(coin_flipping_strategy, self).OnStarted2(time)
self._rng = random.Random()
sec = self.Security
self._price_step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if self._price_step <= 0:
self._price_step = 1.0
self._tp_dist = self.TakeProfitPips * self._price_step
self._sl_dist = self.StopLossPips * self._price_step
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self.Position > 0:
if self._stop_price is not None and low <= self._stop_price:
self.SellMarket()
self._reset_targets()
elif self._take_price is not None and high >= self._take_price:
self.SellMarket()
self._reset_targets()
elif self.Position < 0:
if self._stop_price is not None and high >= self._stop_price:
self.BuyMarket()
self._reset_targets()
elif self._take_price is not None and low <= self._take_price:
self.BuyMarket()
self._reset_targets()
if self.Position != 0:
return
if self._rng is None:
return
if close <= 0:
return
is_buy = self._rng.randint(0, 1) == 0
if is_buy:
self.BuyMarket()
self._entry_price = close
self._stop_price = close - self._sl_dist if self._sl_dist > 0 else None
self._take_price = close + self._tp_dist if self._tp_dist > 0 else None
else:
self.SellMarket()
self._entry_price = close
self._stop_price = close + self._sl_dist if self._sl_dist > 0 else None
self._take_price = close - self._tp_dist if self._tp_dist > 0 else None
def _reset_targets(self):
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def OnReseted(self):
super(coin_flipping_strategy, self).OnReseted()
self._rng = None
self._price_step = 1.0
self._tp_dist = 0.0
self._sl_dist = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def CreateClone(self):
return coin_flipping_strategy()