Asset Growth Effect
取引規則
資産成長率 = 最新公表年度の TotalAssets / 一年前の同じ決算日の TotalAssets − 1。両報告は信号時点で公表済みである必要があります。最低 Quantiles を買い、最高を売り、片側 floor(適格数 / Quantiles) 銘柄。最低 Quantiles 組の報告が必要です。既定は毎年七月です。
毎年 RebalanceMonth の最初の適格な同期確定足で一度リバランスします。後日の公表は過去の判断に影響しません。MaxReportAgeDays は PeriodEnd から数えます。
入力データ
FundamentalsFile は必須 CSV です:SecurityId,PeriodEnd,PublishedAt,TotalAssets,NetIncome,OperatingCashFlow,BookEquity,SharesOutstanding。PeriodEnd は yyyy-MM-dd、PublishedAt は yyyy-MM-ddTHH:mm:ssZ(UTC)。資産と株数は正である必要があります。信号時点で公表済みの最新会計期間と最新改訂だけを選びます。重複改訂や期末より前の公表は禁止です。足から財務値を作りません。
パラメーター
| パラメーター | 既定値 |
|---|---|
Universe |
必須 |
Capital |
100000 |
Leverage |
1 |
MinTradeUsd |
50 |
CandleType |
TimeSpan.FromMinutes(5).TimeFrame() |
Quantiles |
10 |
FundamentalsFile |
必須 |
RebalanceMonth |
7 |
MaxReportAgeDays |
550 |
約定と保有数量
Universe を明示的に設定します。価格と Capital は取引単位当たりの USD です。Capital は固定予算、Leverage は総エクスポージャーを指定します。損切りや利食い注文は追加しません。
買い・売りの各バスケットに Capital × Leverage の半分を均等配分します。信号終値で数量に換算し、VolumeStep 単位でゼロ方向へ丸めます。実際の約定残高を使い、未完了注文を待ち、MinVolume/MaxVolume を守って大口調整を後続足へ分割します。MinTradeUsd は小口調整を除外しますが、全決済には適用しません。
検証
約定検証には既存 BTC/TON アーカイブのみを使います。明示した財務報告、オプション観測、準備価格は仕組みのテスト用で、実企業財務やオプション履歴ではありません。短縮期間、二分位、三月の年次調整は明示的な検証設定で、元の長期既定値の取引を証明しません。収益率は主張しません。
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>Asset Growth Effect with explicit inputs and execution-based portfolio accounting.</summary>
public class AssetGrowthEffectStrategy : Strategy
{
private readonly StrategyParam<IEnumerable<Security>> _universe;
private readonly StrategyParam<decimal> _capital;
private readonly StrategyParam<decimal> _leverage;
private readonly StrategyParam<decimal> _minTradeUsd;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _quantiles;
private readonly StrategyParam<string> _fundamentalsFile;
private readonly StrategyParam<int> _rebalanceMonth;
private readonly StrategyParam<int> _maxReportAgeDays;
private Security[] _securities = [];
private readonly Dictionary<Security, (DateTime time, decimal price)> _latest = [];
private readonly Dictionary<Security, decimal> _targets = [];
private readonly List<Order> _pending = [];
private int _lastRebalance;
private DateTime? _lastBar;
private sealed record Report(string Id, DateTime Period, DateTime Published, decimal Assets, decimal Income, decimal CashFlow, decimal Book, decimal Shares);
private readonly List<Report> _reports = [];
/// <summary>Execution securities with USD prices and valid volume metadata.</summary>
public IEnumerable<Security> Universe { get => _universe.Value; set => _universe.Value = value; }
/// <summary>Fixed gross capital budget in USD.</summary>
public decimal Capital { get => _capital.Value; set => _capital.Value = value; }
/// <summary>Gross target exposure divided by the capital budget.</summary>
public decimal Leverage { get => _leverage.Value; set => _leverage.Value = value; }
/// <summary>Minimum notional adjustment; complete liquidation bypasses this filter.</summary>
public decimal MinTradeUsd { get => _minTradeUsd.Value; set => _minTradeUsd.Value = value; }
/// <summary>Closed candle type; daily statistics count distinct UTC dates.</summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
/// <summary>Number of cross-sectional buckets, requiring at least this many eligible securities.</summary>
public int Quantiles { get => _quantiles.Value; set => _quantiles.Value = value; }
/// <summary>Point-in-time annual financial reports in CSV format.</summary>
public string FundamentalsFile { get => _fundamentalsFile.Value; set => _fundamentalsFile.Value = value; }
/// <summary>UTC calendar month of the annual portfolio rebalance.</summary>
public int RebalanceMonth { get => _rebalanceMonth.Value; set => _rebalanceMonth.Value = value; }
/// <summary>Maximum age of the fiscal period at a rebalance.</summary>
public int MaxReportAgeDays { get => _maxReportAgeDays.Value; set => _maxReportAgeDays.Value = value; }
/// <summary>Initialize the published strategy parameters.</summary>
public AssetGrowthEffectStrategy()
{
_universe = Param<IEnumerable<Security>>(nameof(Universe)).SetRequired()
.SetDisplay("Universe", "Execution securities with USD prices and valid volume metadata", "Parameters");
_capital = Param(nameof(Capital), 100000m).SetGreaterThanZero()
.SetDisplay("Capital", "Fixed gross capital budget in USD", "Parameters");
_leverage = Param(nameof(Leverage), 1m).SetGreaterThanZero()
.SetDisplay("Leverage", "Gross target exposure divided by the capital budget", "Parameters");
_minTradeUsd = Param(nameof(MinTradeUsd), 50m).SetRange(0m, decimal.MaxValue)
.SetDisplay("MinTradeUsd", "Minimum notional adjustment; complete liquidation bypasses this filter", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("CandleType", "Closed candle type; daily statistics count distinct UTC dates", "Parameters");
_quantiles = Param(nameof(Quantiles), 10).SetRange(2, 10000)
.SetDisplay("Quantiles", "Number of cross-sectional buckets, requiring at least this many eligible securities", "Parameters");
_fundamentalsFile = Param<string>(nameof(FundamentalsFile)).SetRequired()
.SetDisplay("FundamentalsFile", "Point-in-time annual financial reports in CSV format", "Parameters");
_rebalanceMonth = Param(nameof(RebalanceMonth), 7).SetRange(1, 12)
.SetDisplay("RebalanceMonth", "UTC calendar month of the annual portfolio rebalance", "Parameters");
_maxReportAgeDays = Param(nameof(MaxReportAgeDays), 550).SetGreaterThanZero()
.SetDisplay("MaxReportAgeDays", "Maximum age of the fiscal period at a rebalance", "Parameters");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
foreach (var security in (Universe ?? []).DistinctBy(s => s.Id, StringComparer.OrdinalIgnoreCase))
yield return (security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_securities = [];
_latest.Clear();
_targets.Clear();
_pending.Clear();
_lastRebalance = 0;
_lastBar = null;
_reports.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
_securities = (Universe ?? []).ToArray();
if (_securities.Length == 0 || _securities.Any(s => s is null || string.IsNullOrWhiteSpace(s.Id))
|| _securities.Select(s => s.Id).Distinct(StringComparer.OrdinalIgnoreCase).Count() != _securities.Length)
throw new InvalidOperationException("Universe must contain distinct execution securities.");
foreach (var security in GetWorkingSecurities().Select(x => x.sec))
{
if (security.VolumeStep is not > 0m || security.MinVolume is not > 0m || security.MaxVolume is not > 0m)
throw new InvalidOperationException("Every security needs positive VolumeStep, MinVolume and MaxVolume.");
}
if (string.IsNullOrWhiteSpace(FundamentalsFile)) throw new InvalidOperationException("FundamentalsFile is required.");
LoadReports();
base.OnStarted2(time);
foreach (var security in GetWorkingSecurities().Select(x => x.sec))
SubscribeCandles(CandleType, security: security).Bind(candle => ProcessCandle(security, candle)).Start();
}
private void ProcessCandle(Security security, ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || candle.ClosePrice <= 0m) return;
_latest[security] = (candle.OpenTime, candle.ClosePrice);
if (_latest.Count != _securities.Length || _latest.Values.Any(v => v.time != candle.OpenTime)
|| _lastBar == candle.OpenTime || !IsFormedAndOnlineAndAllowTrading()) return;
_lastBar = candle.OpenTime;
var month = candle.OpenTime.Year * 100 + candle.OpenTime.Month;
if (_lastRebalance / 100 != candle.OpenTime.Year && SelectTargets(candle.OpenTime))
_lastRebalance = month;
ReconcileTargets();
}
private decimal Units(Security security, decimal notional)
=> Math.Floor(notional / _latest[security].price / security.VolumeStep.Value) * security.VolumeStep.Value;
private void ReconcileTargets()
{
if (_pending.Any(o => o.State is not (OrderStates.Done or OrderStates.Failed))) return;
_pending.Clear();
foreach (var security in _securities)
{
if (!_targets.TryGetValue(security, out var target)) continue;
var delta = target - GetPositionValue(security, Portfolio).GetValueOrDefault();
if (delta == 0m || (target != 0m && Math.Abs(delta) * _latest[security].price < MinTradeUsd)) continue;
var step = security.VolumeStep.Value;
var quantity = Math.Floor(Math.Min(Math.Abs(delta), security.MaxVolume.Value) / step) * step;
if (quantity < security.MinVolume.Value) continue;
var order = new Order
{
Security = security, Portfolio = Portfolio, Type = OrderTypes.Market,
Side = delta > 0m ? Sides.Buy : Sides.Sell, Volume = quantity, Comment = "0353Rebalance",
};
_pending.Add(order);
RegisterOrder(order);
}
}
private void LoadReports()
{
var lines = File.ReadLines(FundamentalsFile).GetEnumerator();
using (lines)
{
if (!lines.MoveNext() || lines.Current.Trim() != "SecurityId,PeriodEnd,PublishedAt,TotalAssets,NetIncome,OperatingCashFlow,BookEquity,SharesOutstanding")
throw new InvalidOperationException("Invalid annual financial report CSV header.");
var keys = new HashSet<(string, DateTime, DateTime)>();
while (lines.MoveNext())
{
if (string.IsNullOrWhiteSpace(lines.Current)) continue;
var row = lines.Current.Split(',');
if (row.Length != 8) throw new InvalidOperationException("A financial report must have eight fields.");
var id = row[0].Trim();
if (!_securities.Any(s => string.Equals(s.Id, id, StringComparison.OrdinalIgnoreCase))) continue;
var period = DateTime.ParseExact(row[1].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture);
var published = DateTime.ParseExact(row[2].Trim(), "yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
var numbers = row.Skip(3).Select(x => decimal.Parse(x, CultureInfo.InvariantCulture)).ToArray();
if (numbers[0] <= 0m || numbers[4] <= 0m || published.Date < period || !keys.Add((id.ToUpperInvariant(), period, published)))
throw new InvalidOperationException("Reports need positive assets/shares, causal publication dates and unique revisions.");
_reports.Add(new(id, period, published, numbers[0], numbers[1], numbers[2], numbers[3], numbers[4]));
}
}
}
private Report FindReport(Security security, DateTime time, DateTime? period = null)
=> _reports.Where(r => string.Equals(r.Id, security.Id, StringComparison.OrdinalIgnoreCase)
&& r.Published <= time && r.Period <= time.Date && (period is null || r.Period == period))
.OrderByDescending(r => r.Period).ThenByDescending(r => r.Published).FirstOrDefault();
private bool SelectTargets(DateTime time)
{
if (time.Month != RebalanceMonth) return false;
var scores = new List<(Security security, decimal score)>();
foreach (var security in _securities)
{
var report = FindReport(security, time);
if (report is null || (time.Date - report.Period).TotalDays > MaxReportAgeDays) continue;
var prior = FindReport(security, time, report.Period.AddYears(-1));
if (prior is null) continue;
var score = report.Assets / prior.Assets - 1m;
scores.Add((security, score));
}
if (scores.Count < Quantiles) return false;
var ranked = scores.OrderBy(x => x.score).ThenBy(x => x.security.Id, StringComparer.Ordinal).Select(x => x.security).ToArray();
var count = ranked.Length / Quantiles;
SetBasketTargets(ranked.Take(count), ranked.TakeLast(count), Capital * Leverage);
return true;
}
private void SetBasketTargets(IEnumerable<Security> longBasket, IEnumerable<Security> shortBasket, decimal grossCapital)
{
var longs = longBasket.ToArray();
var shorts = shortBasket.ToArray();
foreach (var s in _securities) _targets[s] = 0m;
foreach (var s in longs) _targets[s] = Units(s, grossCapital / (2m * longs.Length));
foreach (var s in shorts) _targets[s] = -Units(s, grossCapital / (2m * shorts.Length));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Strategies")
from System import DateTime, Decimal, Math, TimeSpan, InvalidOperationException
from System.Collections.Generic import IEnumerable
from System.Globalization import CultureInfo, DateTimeStyles
from System.IO import File
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Order, Security
from StockSharp.Messages import CandleStates, DataType, OrderStates, OrderTypes, Sides
class asset_growth_effect_strategy(Strategy):
"""Asset Growth Effect with explicit inputs and execution-based portfolio accounting."""
def __init__(self):
super(asset_growth_effect_strategy, self).__init__()
self._universe = self.Param[IEnumerable[Security]]("Universe", None).SetRequired() \
.SetDisplay("Universe", "Execution securities with USD prices and valid volume metadata", "Parameters")
self._capital = self.Param("Capital", Decimal(100000)).SetGreaterThanZero() \
.SetDisplay("Capital", "Fixed gross capital budget in USD", "Parameters")
self._leverage = self.Param("Leverage", Decimal(1)).SetGreaterThanZero() \
.SetDisplay("Leverage", "Gross target exposure divided by the capital budget", "Parameters")
self._min_trade_usd = self.Param("MinTradeUsd", Decimal(50)).SetRange(Decimal(0), Decimal.MaxValue) \
.SetDisplay("MinTradeUsd", "Minimum notional adjustment; complete liquidation bypasses this filter", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("CandleType", "Closed candle type; daily statistics count distinct UTC dates", "Parameters")
self._quantiles = self.Param("Quantiles", 10).SetRange(2, 10000) \
.SetDisplay("Quantiles", "Number of cross-sectional buckets, requiring at least this many eligible securities", "Parameters")
self._fundamentals_file = self.Param[str]("FundamentalsFile", None).SetRequired() \
.SetDisplay("FundamentalsFile", "Point-in-time annual financial reports in CSV format", "Parameters")
self._rebalance_month = self.Param("RebalanceMonth", 7).SetRange(1, 12) \
.SetDisplay("RebalanceMonth", "UTC calendar month of the annual portfolio rebalance", "Parameters")
self._max_report_age_days = self.Param("MaxReportAgeDays", 550).SetGreaterThanZero() \
.SetDisplay("MaxReportAgeDays", "Maximum age of the fiscal period at a rebalance", "Parameters")
self._clear_state()
def _clear_state(self):
self._securities = []
self._latest = {}
self._targets = {}
self._pending = []
self._last_rebalance = 0
self._last_bar = None
self._reports = []
@property
def Universe(self):
return self._universe.Value
@Universe.setter
def Universe(self, value):
self._universe.Value = value
@property
def Capital(self):
return self._capital.Value
@Capital.setter
def Capital(self, value):
self._capital.Value = value
@property
def Leverage(self):
return self._leverage.Value
@Leverage.setter
def Leverage(self, value):
self._leverage.Value = value
@property
def MinTradeUsd(self):
return self._min_trade_usd.Value
@MinTradeUsd.setter
def MinTradeUsd(self, value):
self._min_trade_usd.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def Quantiles(self):
return self._quantiles.Value
@Quantiles.setter
def Quantiles(self, value):
self._quantiles.Value = value
@property
def FundamentalsFile(self):
return self._fundamentals_file.Value
@FundamentalsFile.setter
def FundamentalsFile(self, value):
self._fundamentals_file.Value = value
@property
def RebalanceMonth(self):
return self._rebalance_month.Value
@RebalanceMonth.setter
def RebalanceMonth(self, value):
self._rebalance_month.Value = value
@property
def MaxReportAgeDays(self):
return self._max_report_age_days.Value
@MaxReportAgeDays.setter
def MaxReportAgeDays(self, value):
self._max_report_age_days.Value = value
def GetWorkingSecurities(self):
securities = list(self.Universe) if self.Universe is not None else []
return [(s, self.CandleType) for s in securities]
def OnReseted(self):
super(asset_growth_effect_strategy, self).OnReseted()
self._clear_state()
def OnStarted2(self, time):
self._securities = list(self.Universe) if self.Universe is not None else []
if not self._securities or any(s is None or not s.Id for s in self._securities) or len(set(s.Id.upper() for s in self._securities)) != len(self._securities):
raise InvalidOperationException("Universe must contain distinct execution securities.")
for s, dt in self.GetWorkingSecurities():
if s.VolumeStep is None or s.MinVolume is None or s.MaxVolume is None or s.VolumeStep <= 0 or s.MinVolume <= 0 or s.MaxVolume <= 0:
raise InvalidOperationException("Every security needs positive VolumeStep, MinVolume and MaxVolume.")
if not self.FundamentalsFile:
raise InvalidOperationException("FundamentalsFile is required.")
self._load_reports()
super(asset_growth_effect_strategy, self).OnStarted2(time)
for security, dt in self.GetWorkingSecurities():
self.SubscribeCandles(dt, True, security).Bind(lambda candle, s=security: self._process_candle(s, candle)).Start()
def _process_candle(self, security, candle):
if candle.State != CandleStates.Finished or candle.ClosePrice <= 0:
return
self._latest[security] = (candle.OpenTime, candle.ClosePrice)
if len(self._latest) != len(self._securities) or any(t != candle.OpenTime for t, p in self._latest.values()) or self._last_bar == candle.OpenTime or not self.IsFormedAndOnlineAndAllowTrading():
return
self._last_bar = candle.OpenTime
month = candle.OpenTime.Year * 100 + candle.OpenTime.Month
if self._last_rebalance // 100 != candle.OpenTime.Year and self._select_targets(candle.OpenTime):
self._last_rebalance = month
self._reconcile_targets()
def _units(self, security, notional):
return Math.Floor(notional / self._latest[security][1] / security.VolumeStep) * security.VolumeStep
def _reconcile_targets(self):
if any(o.State != OrderStates.Done and o.State != OrderStates.Failed for o in self._pending):
return
self._pending = []
for security in self._securities:
if security not in self._targets:
continue
target = self._targets[security]
position = self.GetPositionValue(security, self.Portfolio)
delta = target - (position if position is not None else Decimal(0))
if delta == 0 or (target != 0 and abs(delta) * self._latest[security][1] < self.MinTradeUsd):
continue
quantity = Math.Floor(min(abs(delta), security.MaxVolume) / security.VolumeStep) * security.VolumeStep
if quantity < security.MinVolume:
continue
order = Order()
order.Security = security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = Sides.Buy if delta > 0 else Sides.Sell
order.Volume = quantity
order.Comment = "0353Rebalance"
self._pending.append(order)
self.RegisterOrder(order)
def _load_reports(self):
lines = iter(File.ReadLines(self.FundamentalsFile))
if next(lines, "").strip() != "SecurityId,PeriodEnd,PublishedAt,TotalAssets,NetIncome,OperatingCashFlow,BookEquity,SharesOutstanding":
raise InvalidOperationException("Invalid annual financial report CSV header.")
keys = set()
for line in lines:
if not line.strip():
continue
row = line.split(",")
if len(row) != 8:
raise InvalidOperationException("A financial report must have eight fields.")
security_id = row[0].strip()
if not any(s.Id.lower() == security_id.lower() for s in self._securities):
continue
period = DateTime.ParseExact(row[1].strip(), "yyyy-MM-dd", CultureInfo.InvariantCulture)
published = DateTime.ParseExact(row[2].strip(), "yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal)
values = [Decimal.Parse(v, CultureInfo.InvariantCulture) for v in row[3:]]
key = (security_id.upper(), period, published)
if values[0] <= 0 or values[4] <= 0 or published.Date < period or key in keys:
raise InvalidOperationException("Reports need positive assets/shares, causal publication dates and unique revisions.")
keys.add(key)
self._reports.append((security_id, period, published) + tuple(values))
def _find_report(self, security, time, period=None):
eligible = [r for r in self._reports if r[0].lower() == security.Id.lower()
and r[2] <= time and r[1] <= time.Date and (period is None or r[1] == period)]
return max(eligible, key=lambda r: (r[1], r[2])) if eligible else None
def _select_targets(self, time):
if time.Month != self.RebalanceMonth:
return False
scores = []
for security in self._securities:
report = self._find_report(security, time)
if report is None or (time.Date - report[1]).TotalDays > self.MaxReportAgeDays:
continue
prior = self._find_report(security, time, report[1].AddYears(-1))
if prior is None:
continue
score = report[3] / prior[3] - Decimal(1)
scores.append((security, score))
if len(scores) < self.Quantiles:
return False
ranked = [s for s, score in sorted(scores, key=lambda x: (x[1], x[0].Id))]
count = len(ranked) // self.Quantiles
self._set_basket_targets(ranked[:count], ranked[-count:], self.Capital * self.Leverage)
return True
def _set_basket_targets(self, longs, shorts, gross_capital):
self._targets = dict((s, Decimal(0)) for s in self._securities)
for s in longs:
self._targets[s] = self._units(s, gross_capital / Decimal(2 * len(longs)))
for s in shorts:
self._targets[s] = -self._units(s, gross_capital / Decimal(2 * len(shorts)))
def CreateClone(self):
return asset_growth_effect_strategy()