Add C# (.NET) binding over the C ABI hub (#226)
The first language stecker on the C ABI hub: a .NET binding exposing all 514 indicators as idiomatic `IDisposable` classes, generated from `wickra.h`. ## What's here - **`bindings/csharp/`** — the `Wickra` .NET 8 package. `[LibraryImport]` source-generated P/Invoke (`NativeMethods.g.cs`) plus idiomatic wrappers (`Indicators.g.cs`), both generated from the committed `bindings/c/include/wickra.h`. The binding owns no indicator maths — it only marshals types across the C ABI. - **Marshalling, verified end-to-end against the native library.** Opaque handles cross as `nint` kept alive per call via a `SafeHandle`; `bool` as `[MarshalAs(U1)]` (Rust `bool` is one byte); a self-correcting `DllImportResolver` validates the loaded library actually exports the Wickra ABI. Tests cover one representative per FFI archetype (scalar, candle, pairwise, multi-output, bars, profile, values-profile, order-book / array-input) plus exact Sma reference values. - **NuGet packaging** — `dotnet pack` produces `Wickra.<version>.nupkg`; the release pipeline stages prebuilt native libraries under `runtimes/<rid>/native/` for six target triples (win/linux/osx × x64/arm64). - **`examples/csharp/`** — nine examples mirroring `examples/c/`: streaming, backtest, multi_timeframe, parallel_assets, three strategies, and fetch_btcusdt + live_binance. - **CI** — a `csharp` job on the three OSes builds the C ABI, tests the binding, and runs the offline examples. **Release** — a gated `csharp-publish` job packs and pushes to NuGet (gated on `NUGET_API_KEY`, independent of the GitHub-release job so a C# hiccup never blocks the C/C++ asset release). - **Docs consistency wave** — README, CONTRIBUTING, CHANGELOG, examples/README, the issue / PR templates, `sync-about.yml`, and `.gitattributes`. The native Python / Node / WASM bindings and the C ABI are untouched; this is additive. Publishing to NuGet stays gated behind the release tag and the secret.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# .NET build output
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
|
||||
# Data fetched at runtime by fetch_btcusdt
|
||||
**/data/
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project>
|
||||
|
||||
<!-- Shared settings + references for every C# example. Each example project
|
||||
only declares <OutputType>Exe</OutputType>. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\bindings\csharp\Wickra\Wickra.csproj" />
|
||||
<Compile Include="..\_common\MarketData.cs" Link="_common\MarketData.cs" />
|
||||
<Compile Include="..\_common\Backtest.cs" Link="_common\Backtest.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
# Wickra examples — C# / .NET
|
||||
|
||||
Runnable .NET examples for the [Wickra .NET binding](../../bindings/csharp).
|
||||
Each example is a small console project that references the `Wickra` project and
|
||||
resolves the native library automatically (from `target/release` during local
|
||||
development, or the NuGet `runtimes/` layout when packaged).
|
||||
|
||||
Build the native library first, then run any example:
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
dotnet run --project examples/csharp/streaming
|
||||
```
|
||||
|
||||
| Example | What it does | Run |
|
||||
| --- | --- | --- |
|
||||
| `streaming` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `dotnet run --project examples/csharp/streaming` |
|
||||
| `backtest` | Compute a basket of indicators over an OHLCV series and print a summary. | `dotnet run --project examples/csharp/backtest -- <ohlcv.csv>` |
|
||||
| `multi_timeframe` | Resample a 1-minute series into 5m / 15m and print an indicator per timeframe. | `dotnet run --project examples/csharp/multi_timeframe` |
|
||||
| `parallel_assets` | SMA(20) batch over a panel of assets, serial vs `Parallel.For`, with speedup. | `dotnet run -c Release --project examples/csharp/parallel_assets -- 200 5000` |
|
||||
| `strategy_rsi_mean_reversion` | RSI(14) mean-reversion with a PnL / Sharpe / max-DD summary. | `dotnet run -c Release --project examples/csharp/strategy_rsi_mean_reversion` |
|
||||
| `strategy_macd_adx` | MACD crossover entries gated by ADX(14) > 20. | `dotnet run -c Release --project examples/csharp/strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze` | Bollinger-squeeze breakout with an ATR(14) trailing stop. | `dotnet run -c Release --project examples/csharp/strategy_bollinger_squeeze` |
|
||||
| `fetch_btcusdt` | Download real BTCUSDT klines from the Binance REST API into a CSV. | `dotnet run --project examples/csharp/fetch_btcusdt` |
|
||||
| `live_binance` | Stream live Binance klines through EMA(20) over a WebSocket. | `dotnet run --project examples/csharp/live_binance` |
|
||||
|
||||
`fetch_btcusdt` and `live_binance` require network access; the rest run offline
|
||||
on deterministic synthetic data. Shared helpers (synthetic data, CSV loader,
|
||||
equity summary) live in [`_common/`](_common).
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace Wickra.Examples;
|
||||
|
||||
/// <summary>Summary statistics for a long-only equity curve.</summary>
|
||||
public sealed record EquityResult(double TotalReturnPct, double Sharpe, double MaxDrawdownPct, int Trades, double FinalEquity);
|
||||
|
||||
/// <summary>
|
||||
/// Minimal long-only backtest helper: turn a stream of per-bar fractional
|
||||
/// returns into a PnL / Sharpe / max-drawdown summary. The strategy examples
|
||||
/// produce the returns; this aggregates them.
|
||||
/// </summary>
|
||||
public static class Backtest
|
||||
{
|
||||
/// <param name="periodReturns">Per-bar fractional returns (0.01 == +1%).</param>
|
||||
/// <param name="trades">Number of position entries.</param>
|
||||
/// <param name="periodsPerYear">Annualisation factor for the Sharpe ratio.</param>
|
||||
public static EquityResult Summarize(IReadOnlyList<double> periodReturns, int trades, double periodsPerYear = 252.0)
|
||||
{
|
||||
double equity = 1.0, peak = 1.0, maxDrawdown = 0.0;
|
||||
foreach (var r in periodReturns)
|
||||
{
|
||||
equity *= 1.0 + r;
|
||||
peak = Math.Max(peak, equity);
|
||||
if (peak > 0)
|
||||
{
|
||||
maxDrawdown = Math.Max(maxDrawdown, (peak - equity) / peak);
|
||||
}
|
||||
}
|
||||
|
||||
var mean = periodReturns.Count > 0 ? periodReturns.Average() : 0.0;
|
||||
var variance = periodReturns.Count > 1
|
||||
? periodReturns.Sum(x => (x - mean) * (x - mean)) / (periodReturns.Count - 1)
|
||||
: 0.0;
|
||||
var stdDev = Math.Sqrt(variance);
|
||||
var sharpe = stdDev > 1e-12 ? mean / stdDev * Math.Sqrt(periodsPerYear) : 0.0;
|
||||
|
||||
return new EquityResult((equity - 1.0) * 100.0, sharpe, maxDrawdown * 100.0, trades, equity);
|
||||
}
|
||||
|
||||
/// <summary>Prints a one-line summary.</summary>
|
||||
public static void Print(string name, EquityResult r)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"{name,-26} return={r.TotalReturnPct,8:F2}% sharpe={r.Sharpe,6:F2} maxDD={r.MaxDrawdownPct,6:F2}% trades={r.Trades}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
namespace Wickra.Examples;
|
||||
|
||||
/// <summary>One OHLCV bar with a millisecond timestamp.</summary>
|
||||
public readonly record struct Bar(double Open, double High, double Low, double Close, double Volume, long Timestamp);
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic synthetic market data plus a small OHLCV CSV loader, shared by
|
||||
/// the offline examples so they run without network access.
|
||||
/// </summary>
|
||||
public static class MarketData
|
||||
{
|
||||
/// <summary>A reproducible price path (trend + two cycles), no randomness.</summary>
|
||||
public static double[] SyntheticPrices(int count, double start = 100.0)
|
||||
{
|
||||
var prices = new double[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
prices[i] = start + 12.0 * Math.Sin(i * 0.05) + 5.0 * Math.Sin(i * 0.013) + i * 0.01;
|
||||
}
|
||||
|
||||
return prices;
|
||||
}
|
||||
|
||||
/// <summary>A reproducible OHLCV series derived from <see cref="SyntheticPrices"/>.</summary>
|
||||
public static Bar[] SyntheticCandles(int count, long startTimestamp = 0, long stepMs = 3_600_000)
|
||||
{
|
||||
var prices = SyntheticPrices(count + 1);
|
||||
var bars = new Bar[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var open = prices[i];
|
||||
var close = prices[i + 1];
|
||||
var high = Math.Max(open, close) + 0.5 + Math.Abs(Math.Sin(i * 0.7));
|
||||
var low = Math.Min(open, close) - 0.5 - Math.Abs(Math.Cos(i * 0.7));
|
||||
var volume = 1_000.0 + 500.0 * (1.0 + Math.Sin(i * 0.1));
|
||||
bars[i] = new Bar(open, high, low, close, volume, startTimestamp + i * stepMs);
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an OHLCV CSV. Accepts rows of <c>timestamp,open,high,low,close,volume</c>
|
||||
/// or <c>open,high,low,close,volume</c>; a non-numeric first row is treated as a header.
|
||||
/// </summary>
|
||||
public static Bar[] LoadOhlcvCsv(string path)
|
||||
{
|
||||
var bars = new List<Bar>();
|
||||
foreach (var rawLine in File.ReadLines(path))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var cols = line.Split(',');
|
||||
if (!double.TryParse(cols[0], System.Globalization.CultureInfo.InvariantCulture, out _) &&
|
||||
!long.TryParse(cols[0], out _))
|
||||
{
|
||||
continue; // header row
|
||||
}
|
||||
|
||||
double F(int i) => double.Parse(cols[i], System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
if (cols.Length >= 6)
|
||||
{
|
||||
bars.Add(new Bar(F(1), F(2), F(3), F(4), F(5), long.Parse(cols[0])));
|
||||
}
|
||||
else
|
||||
{
|
||||
bars.Add(new Bar(F(0), F(1), F(2), F(3), F(4), bars.Count));
|
||||
}
|
||||
}
|
||||
|
||||
return bars.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Wickra;
|
||||
using Wickra.Examples;
|
||||
|
||||
// Compute a basket of indicators over an OHLCV series and print a summary.
|
||||
// Pass a CSV path (timestamp,open,high,low,close,volume) or run on synthetic data.
|
||||
var source = args.Length > 0 ? args[0] : "synthetic";
|
||||
Bar[] bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(1000);
|
||||
|
||||
Console.WriteLine($"Backtest over {bars.Length} bars ({source}):");
|
||||
|
||||
using var sma = new Sma(20);
|
||||
using var ema = new Ema(50);
|
||||
using var rsi = new Rsi(14);
|
||||
using var atr = new Atr(14);
|
||||
|
||||
double lastSma = 0, lastEma = 0, lastRsi = 0, lastAtr = 0;
|
||||
var oversold = 0;
|
||||
foreach (var b in bars)
|
||||
{
|
||||
lastSma = sma.Update(b.Close);
|
||||
lastEma = ema.Update(b.Close);
|
||||
lastRsi = rsi.Update(b.Close);
|
||||
lastAtr = atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
|
||||
if (double.IsFinite(lastRsi) && lastRsi < 30.0)
|
||||
{
|
||||
oversold++;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($" SMA(20) last = {lastSma:F4}");
|
||||
Console.WriteLine($" EMA(50) last = {lastEma:F4}");
|
||||
Console.WriteLine($" RSI(14) last = {lastRsi:F4} ({oversold} oversold bars)");
|
||||
Console.WriteLine($" ATR(14) last = {lastAtr:F4}");
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
// Download real BTCUSDT hourly klines from the Binance REST API into a CSV that the
|
||||
// other examples can consume. Requires network access (build-only in CI).
|
||||
const string url = "https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1h&limit=500";
|
||||
|
||||
using var http = new HttpClient();
|
||||
Console.WriteLine($"Fetching {url}");
|
||||
var json = await http.GetStringAsync(url);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var dir = Path.Combine(AppContext.BaseDirectory, "data");
|
||||
Directory.CreateDirectory(dir);
|
||||
var path = Path.Combine(dir, "btcusdt_1h.csv");
|
||||
|
||||
using var writer = new StreamWriter(path);
|
||||
writer.WriteLine("timestamp,open,high,low,close,volume");
|
||||
var count = 0;
|
||||
foreach (var kline in doc.RootElement.EnumerateArray())
|
||||
{
|
||||
// Binance kline array: [openTime, open, high, low, close, volume, ...]
|
||||
var ts = kline[0].GetInt64();
|
||||
var o = kline[1].GetString();
|
||||
var h = kline[2].GetString();
|
||||
var l = kline[3].GetString();
|
||||
var c = kline[4].GetString();
|
||||
var v = kline[5].GetString();
|
||||
writer.WriteLine(string.Create(CultureInfo.InvariantCulture, $"{ts},{o},{h},{l},{c},{v}"));
|
||||
count++;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Wrote {count} klines to {path}");
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Globalization;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Wickra;
|
||||
|
||||
// Stream live BTCUSDT 1-minute klines from Binance and feed each close through EMA(20).
|
||||
// Requires network access (build-only in CI). Runs for up to 60 seconds.
|
||||
var uri = new Uri("wss://stream.binance.com:9443/ws/btcusdt@kline_1m");
|
||||
Console.WriteLine($"Connecting to {uri} (up to 60s)...");
|
||||
|
||||
using var ws = new ClientWebSocket();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
using var ema = new Ema(20);
|
||||
var buffer = new byte[8192];
|
||||
|
||||
try
|
||||
{
|
||||
await ws.ConnectAsync(uri, cts.Token);
|
||||
while (ws.State == WebSocketState.Open && !cts.IsCancellationRequested)
|
||||
{
|
||||
var result = await ws.ReceiveAsync(buffer, cts.Token);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||
if (doc.RootElement.TryGetProperty("k", out var k))
|
||||
{
|
||||
var close = double.Parse(k.GetProperty("c").GetString()!, CultureInfo.InvariantCulture);
|
||||
var value = ema.Update(close);
|
||||
Console.WriteLine($"close={close:F2} EMA(20)={value:F2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("Done (time limit reached).");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,44 @@
|
||||
using Wickra;
|
||||
using Wickra.Examples;
|
||||
|
||||
// Resample a 1-minute series into higher timeframes and run an indicator per timeframe.
|
||||
var oneMinute = MarketData.SyntheticCandles(1200, startTimestamp: 0, stepMs: 60_000);
|
||||
|
||||
Console.WriteLine("EMA(20) of close across timeframes (resampled from 1-minute bars):");
|
||||
foreach (var factor in new[] { 1, 5, 15 })
|
||||
{
|
||||
var bars = Resample(oneMinute, factor);
|
||||
using var ema = new Ema(20);
|
||||
double last = 0;
|
||||
foreach (var b in bars)
|
||||
{
|
||||
last = ema.Update(b.Close);
|
||||
}
|
||||
|
||||
Console.WriteLine($" {factor,2}m: {bars.Length,5} bars EMA(20) last = {last:F4}");
|
||||
}
|
||||
|
||||
static Bar[] Resample(Bar[] source, int factor)
|
||||
{
|
||||
if (factor <= 1)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
var output = new List<Bar>();
|
||||
for (var i = 0; i < source.Length; i += factor)
|
||||
{
|
||||
var end = Math.Min(i + factor, source.Length);
|
||||
double high = double.MinValue, low = double.MaxValue, volume = 0;
|
||||
for (var j = i; j < end; j++)
|
||||
{
|
||||
high = Math.Max(high, source[j].High);
|
||||
low = Math.Min(low, source[j].Low);
|
||||
volume += source[j].Volume;
|
||||
}
|
||||
|
||||
output.Add(new Bar(source[i].Open, high, low, source[end - 1].Close, volume, source[i].Timestamp));
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Diagnostics;
|
||||
using Wickra;
|
||||
using Wickra.Examples;
|
||||
|
||||
// Run SMA(20) batch over a panel of assets, serial vs Parallel.For, and report the speedup.
|
||||
var assets = args.Length > 0 ? int.Parse(args[0]) : 500;
|
||||
var bars = args.Length > 1 ? int.Parse(args[1]) : 20_000;
|
||||
|
||||
var panel = new double[assets][];
|
||||
for (var a = 0; a < assets; a++)
|
||||
{
|
||||
panel[a] = MarketData.SyntheticPrices(bars, start: 50.0 + a * 0.1);
|
||||
}
|
||||
|
||||
// Warm up the JIT and thread pool so the comparison is fair.
|
||||
using (var warm = new Sma(20))
|
||||
{
|
||||
warm.Batch(panel[0]);
|
||||
}
|
||||
|
||||
var sink = 0.0;
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var a = 0; a < assets; a++)
|
||||
{
|
||||
using var sma = new Sma(20);
|
||||
var result = sma.Batch(panel[a]);
|
||||
sink += result[^1];
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
var serialMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
var lasts = new double[assets];
|
||||
sw.Restart();
|
||||
Parallel.For(0, assets, a =>
|
||||
{
|
||||
using var sma = new Sma(20);
|
||||
var result = sma.Batch(panel[a]);
|
||||
lasts[a] = result[^1];
|
||||
});
|
||||
sw.Stop();
|
||||
var parallelMs = sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
Console.WriteLine($"{assets} assets x {bars} bars, SMA(20) batch:");
|
||||
Console.WriteLine($" serial {serialMs,8:F1} ms");
|
||||
Console.WriteLine($" parallel {parallelMs,8:F1} ms ({serialMs / Math.Max(parallelMs, 1e-9):F1}x speedup)");
|
||||
GC.KeepAlive(sink);
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,46 @@
|
||||
using Wickra;
|
||||
using Wickra.Examples;
|
||||
|
||||
// Breakout: when Bollinger bandwidth is tight (a "squeeze") and price closes above the
|
||||
// upper band, go long with an ATR(14) trailing stop.
|
||||
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
|
||||
|
||||
using var bollinger = new BollingerBands(20, 2.0);
|
||||
using var atr = new Atr(14);
|
||||
|
||||
var returns = new List<double>();
|
||||
var trades = 0;
|
||||
var inPosition = false;
|
||||
var entry = 0.0;
|
||||
var stop = 0.0;
|
||||
|
||||
foreach (var b in bars)
|
||||
{
|
||||
var bands = bollinger.Update(b.Close);
|
||||
var atrValue = atr.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
|
||||
if (bands is not { } band || !double.IsFinite(atrValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bandwidth = band.Middle != 0.0 ? (band.Upper - band.Lower) / band.Middle : double.MaxValue;
|
||||
|
||||
if (!inPosition && bandwidth < 0.06 && b.Close > band.Upper)
|
||||
{
|
||||
inPosition = true;
|
||||
entry = b.Close;
|
||||
stop = b.Close - 2.0 * atrValue;
|
||||
trades++;
|
||||
}
|
||||
else if (inPosition)
|
||||
{
|
||||
stop = Math.Max(stop, b.Close - 2.0 * atrValue); // trail the stop up
|
||||
if (b.Close < stop)
|
||||
{
|
||||
returns.Add((b.Close - entry) / entry);
|
||||
inPosition = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Backtest.Print("Bollinger squeeze", Backtest.Summarize(returns, trades));
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,42 @@
|
||||
using Wickra;
|
||||
using Wickra.Examples;
|
||||
|
||||
// Trend follower: enter long on a MACD histogram cross up, but only when ADX(14) > 20
|
||||
// confirms a trend; exit when the histogram crosses back below zero.
|
||||
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
|
||||
|
||||
using var macd = new MacdIndicator(12, 26, 9);
|
||||
using var adx = new Adx(14);
|
||||
|
||||
var returns = new List<double>();
|
||||
var trades = 0;
|
||||
var inPosition = false;
|
||||
var entry = 0.0;
|
||||
var prevHistogram = double.NaN;
|
||||
|
||||
foreach (var b in bars)
|
||||
{
|
||||
var m = macd.Update(b.Close);
|
||||
var a = adx.Update(b.Open, b.High, b.Low, b.Close, b.Volume, b.Timestamp);
|
||||
if (m is not { } macdValue || a is not { } adxValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var trending = adxValue.Adx > 20.0;
|
||||
if (!inPosition && trending && double.IsFinite(prevHistogram) && prevHistogram <= 0.0 && macdValue.Histogram > 0.0)
|
||||
{
|
||||
inPosition = true;
|
||||
entry = b.Close;
|
||||
trades++;
|
||||
}
|
||||
else if (inPosition && macdValue.Histogram < 0.0)
|
||||
{
|
||||
returns.Add((b.Close - entry) / entry);
|
||||
inPosition = false;
|
||||
}
|
||||
|
||||
prevHistogram = macdValue.Histogram;
|
||||
}
|
||||
|
||||
Backtest.Print("MACD + ADX trend", Backtest.Summarize(returns, trades));
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,34 @@
|
||||
using Wickra;
|
||||
using Wickra.Examples;
|
||||
|
||||
// Mean reversion: go long when RSI(14) drops below 30, exit when it recovers above 50.
|
||||
var bars = args.Length > 0 ? MarketData.LoadOhlcvCsv(args[0]) : MarketData.SyntheticCandles(2000);
|
||||
|
||||
using var rsi = new Rsi(14);
|
||||
var returns = new List<double>();
|
||||
var trades = 0;
|
||||
var inPosition = false;
|
||||
var entry = 0.0;
|
||||
|
||||
foreach (var b in bars)
|
||||
{
|
||||
var value = rsi.Update(b.Close);
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inPosition && value < 30.0)
|
||||
{
|
||||
inPosition = true;
|
||||
entry = b.Close;
|
||||
trades++;
|
||||
}
|
||||
else if (inPosition && value > 50.0)
|
||||
{
|
||||
returns.Add((b.Close - entry) / entry);
|
||||
inPosition = false;
|
||||
}
|
||||
}
|
||||
|
||||
Backtest.Print("RSI mean-reversion", Backtest.Summarize(returns, trades));
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
using Wickra;
|
||||
using Wickra.Examples;
|
||||
|
||||
// Feed a synthetic price series through several indicators tick by tick (O(1) each).
|
||||
var prices = MarketData.SyntheticPrices(500);
|
||||
|
||||
using var sma = new Sma(20);
|
||||
using var ema = new Ema(20);
|
||||
using var rsi = new Rsi(14);
|
||||
using var macd = new MacdIndicator(12, 26, 9);
|
||||
|
||||
double lastSma = 0, lastEma = 0, lastRsi = 0;
|
||||
MacdOutput? lastMacd = null;
|
||||
foreach (var price in prices)
|
||||
{
|
||||
lastSma = sma.Update(price);
|
||||
lastEma = ema.Update(price);
|
||||
lastRsi = rsi.Update(price);
|
||||
lastMacd = macd.Update(price);
|
||||
}
|
||||
|
||||
Console.WriteLine($"Streamed {prices.Length} prices through SMA(20), EMA(20), RSI(14), MACD(12,26,9):");
|
||||
Console.WriteLine($" SMA = {lastSma:F4}");
|
||||
Console.WriteLine($" EMA = {lastEma:F4}");
|
||||
Console.WriteLine($" RSI = {lastRsi:F4}");
|
||||
if (lastMacd is { } m)
|
||||
{
|
||||
Console.WriteLine($" MACD = {m.Macd:F4} signal={m.Signal:F4} hist={m.Histogram:F4}");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user