mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 03:28:05 +00:00
updates from mac
This commit is contained in:
+67
-67
@@ -1,67 +1,67 @@
|
||||
# GBM Class
|
||||
|
||||
`GBM` (Geometric Brownian Motion) is a synthetic data generator that simulates realistic financial price movements. It is useful for testing indicators, strategies, and system performance without relying on external data files.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Geometric Brownian Motion**: Uses the standard mathematical model for asset price dynamics.
|
||||
- **Configurable Parameters**: Control drift (trend) and volatility (noise).
|
||||
- **Stateless Design**: Minimal memory footprint; only maintains state needed for continuity.
|
||||
- **Dual Modes**: Supports both streaming (bar-by-bar) and batch generation.
|
||||
- **Intra-bar Updates**: Can simulate real-time price updates within a single bar.
|
||||
|
||||
## Mathematical Model
|
||||
|
||||
The price evolution follows the stochastic differential equation:
|
||||
|
||||
$$ dS_t = \mu S_t dt + \sigma S_t dW_t $$
|
||||
|
||||
Where:
|
||||
- $S_t$: Asset price at time $t$
|
||||
- $\mu$: Drift (expected return)
|
||||
- $\sigma$: Volatility (standard deviation of returns)
|
||||
- $W_t$: Wiener process (Brownian motion)
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class GBM : IFeed
|
||||
{
|
||||
public GBM(double startPrice = 100.0, double mu = 0.05, double sigma = 0.2, TimeSpan? defaultTimeframe = null);
|
||||
|
||||
public TBar Next(bool isNew = true);
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Initialization
|
||||
```csharp
|
||||
// Default: Start at 100, 5% drift, 20% volatility
|
||||
var gbm = new GBM();
|
||||
|
||||
// Custom: Start at 50, 10% drift, 50% volatility
|
||||
var volatileGbm = new GBM(startPrice: 50.0, mu: 0.10, sigma: 0.50);
|
||||
```
|
||||
|
||||
### 2. Streaming Generation
|
||||
```csharp
|
||||
// Generate a new bar
|
||||
var bar = gbm.Next(isNew: true);
|
||||
|
||||
// Simulate intra-bar updates (e.g., real-time ticks)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var updatedBar = gbm.Next(isNew: false);
|
||||
Console.WriteLine($"Update: {updatedBar.Close}");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Batch Generation
|
||||
```csharp
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
// Generate 1000 bars
|
||||
var history = gbm.Fetch(1000, startTime, interval);
|
||||
# GBM Class
|
||||
|
||||
`GBM` (Geometric Brownian Motion) is a synthetic data generator that simulates realistic financial price movements. It is useful for testing indicators, strategies, and system performance without relying on external data files.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Geometric Brownian Motion**: Uses the standard mathematical model for asset price dynamics.
|
||||
- **Configurable Parameters**: Control drift (trend) and volatility (noise).
|
||||
- **Stateless Design**: Minimal memory footprint; only maintains state needed for continuity.
|
||||
- **Dual Modes**: Supports both streaming (bar-by-bar) and batch generation.
|
||||
- **Intra-bar Updates**: Can simulate real-time price updates within a single bar.
|
||||
|
||||
## Mathematical Model
|
||||
|
||||
The price evolution follows the stochastic differential equation:
|
||||
|
||||
$$ dS_t = \mu S_t dt + \sigma S_t dW_t $$
|
||||
|
||||
Where:
|
||||
- $S_t$: Asset price at time $t$
|
||||
- $\mu$: Drift (expected return)
|
||||
- $\sigma$: Volatility (standard deviation of returns)
|
||||
- $W_t$: Wiener process (Brownian motion)
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class GBM : IFeed
|
||||
{
|
||||
public GBM(double startPrice = 100.0, double mu = 0.05, double sigma = 0.2, TimeSpan? defaultTimeframe = null);
|
||||
|
||||
public TBar Next(bool isNew = true);
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Initialization
|
||||
```csharp
|
||||
// Default: Start at 100, 5% drift, 20% volatility
|
||||
var gbm = new GBM();
|
||||
|
||||
// Custom: Start at 50, 10% drift, 50% volatility
|
||||
var volatileGbm = new GBM(startPrice: 50.0, mu: 0.10, sigma: 0.50);
|
||||
```
|
||||
|
||||
### 2. Streaming Generation
|
||||
```csharp
|
||||
// Generate a new bar
|
||||
var bar = gbm.Next(isNew: true);
|
||||
|
||||
// Simulate intra-bar updates (e.g., real-time ticks)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var updatedBar = gbm.Next(isNew: false);
|
||||
Console.WriteLine($"Update: {updatedBar.Close}");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Batch Generation
|
||||
```csharp
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
// Generate 1000 bars
|
||||
var history = gbm.Fetch(1000, startTime, interval);
|
||||
|
||||
+283
-283
@@ -1,283 +1,283 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class GBMTests
|
||||
{
|
||||
[Fact]
|
||||
public void Next_DefaultParameter_GeneratesNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewTrue_AdvancesToNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
var bar2 = gbm.Next(isNew: true);
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
|
||||
var bar2 = gbm.Next(isNew: false);
|
||||
|
||||
Assert.Equal(initialTime, bar2.Time);
|
||||
// Price likely changed (GBM random walk)
|
||||
Assert.NotEqual(bar1.Close, bar2.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_RefBool_HonorsRequest()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// GBM always honors isNew - parameter should remain unchanged
|
||||
bool isNew1 = true;
|
||||
var bar1 = gbm.Next(ref isNew1);
|
||||
Assert.True(isNew1, "GBM should honor isNew=true request");
|
||||
|
||||
bool isNew2 = false;
|
||||
long time1 = bar1.Time;
|
||||
var bar2 = gbm.Next(ref isNew2);
|
||||
Assert.False(isNew2, "GBM should honor isNew=false request");
|
||||
Assert.Equal(time1, bar2.Time);
|
||||
|
||||
bool isNew3 = true;
|
||||
var bar3 = gbm.Next(ref isNew3);
|
||||
Assert.True(isNew3, "GBM should honor isNew=true request");
|
||||
Assert.NotEqual(time1, bar3.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesCorrectCount()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
int count = 10;
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(count, startTime, interval);
|
||||
|
||||
Assert.Equal(count, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesSequentialBars()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify time sequence
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
Assert.True(series[i].Time > series[i - 1].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_RespectsInterval()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var interval = TimeSpan.FromHours(1);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify interval spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_StartsAtSpecifiedTime()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(5);
|
||||
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.Equal(startTime, series[0].Time);
|
||||
Assert.Equal(startTime + interval.Ticks, series[1].Time);
|
||||
Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithDifferentIntervals_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Test different intervals
|
||||
var intervals = new[] {
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(5),
|
||||
TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
foreach (var interval in intervals)
|
||||
{
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
// Verify spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratesRealisticOHLCV()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(10, startTime, interval);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var bar = series[i];
|
||||
|
||||
// High should be >= max(Open, Close)
|
||||
Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
|
||||
|
||||
// Low should be <= min(Open, Close)
|
||||
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
|
||||
|
||||
// Volume should be positive
|
||||
Assert.True(bar.Volume > 0);
|
||||
|
||||
// All prices should be positive
|
||||
Assert.True(bar.Open > 0);
|
||||
Assert.True(bar.High > 0);
|
||||
Assert.True(bar.Low > 0);
|
||||
Assert.True(bar.Close > 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntraBarUpdates_ModifyCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
double initialClose = bar1.Close;
|
||||
|
||||
// Loop until price changes (random walk might stay same but unlikely)
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
Assert.Equal(initialTime, bar.Time);
|
||||
if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(changed, "Price should change during intra-bar updates");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedStreamingAndBatch_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Start with streaming
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// Batch generation with explicit time
|
||||
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.True(series[0].Time > bar2.Time);
|
||||
Assert.Equal(3, series.Count);
|
||||
|
||||
// Continue streaming after batch (uses internal state)
|
||||
var bar3 = gbm.Next();
|
||||
Assert.True(bar3.Time > series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriftAndVolatility_AffectPriceMovement()
|
||||
{
|
||||
// High volatility should produce more price variation
|
||||
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
|
||||
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
|
||||
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
|
||||
var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
|
||||
|
||||
// Calculate price ranges
|
||||
double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
|
||||
double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
|
||||
|
||||
// High volatility should generally produce larger absolute movements
|
||||
Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsecutiveCalls_MaintainContinuity()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// bar2.Open should equal bar1.Close (continuity)
|
||||
Assert.Equal(bar1.Close, bar2.Open);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stateless_NoHistoryStorage()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Generate multiple bars
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
gbm.Next();
|
||||
}
|
||||
|
||||
// GBM should not expose any history storage
|
||||
var type = gbm.GetType();
|
||||
var barsProperty = type.GetProperty("Bars");
|
||||
|
||||
Assert.Null(barsProperty);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class GBMTests
|
||||
{
|
||||
[Fact]
|
||||
public void Next_DefaultParameter_GeneratesNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewTrue_AdvancesToNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
var bar2 = gbm.Next(isNew: true);
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
|
||||
var bar2 = gbm.Next(isNew: false);
|
||||
|
||||
Assert.Equal(initialTime, bar2.Time);
|
||||
// Price likely changed (GBM random walk)
|
||||
Assert.NotEqual(bar1.Close, bar2.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_RefBool_HonorsRequest()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// GBM always honors isNew - parameter should remain unchanged
|
||||
bool isNew1 = true;
|
||||
var bar1 = gbm.Next(ref isNew1);
|
||||
Assert.True(isNew1, "GBM should honor isNew=true request");
|
||||
|
||||
bool isNew2 = false;
|
||||
long time1 = bar1.Time;
|
||||
var bar2 = gbm.Next(ref isNew2);
|
||||
Assert.False(isNew2, "GBM should honor isNew=false request");
|
||||
Assert.Equal(time1, bar2.Time);
|
||||
|
||||
bool isNew3 = true;
|
||||
var bar3 = gbm.Next(ref isNew3);
|
||||
Assert.True(isNew3, "GBM should honor isNew=true request");
|
||||
Assert.NotEqual(time1, bar3.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesCorrectCount()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
int count = 10;
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(count, startTime, interval);
|
||||
|
||||
Assert.Equal(count, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesSequentialBars()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify time sequence
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
Assert.True(series[i].Time > series[i - 1].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_RespectsInterval()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var interval = TimeSpan.FromHours(1);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify interval spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_StartsAtSpecifiedTime()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(5);
|
||||
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.Equal(startTime, series[0].Time);
|
||||
Assert.Equal(startTime + interval.Ticks, series[1].Time);
|
||||
Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithDifferentIntervals_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Test different intervals
|
||||
var intervals = new[] {
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(5),
|
||||
TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
foreach (var interval in intervals)
|
||||
{
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
// Verify spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratesRealisticOHLCV()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(10, startTime, interval);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var bar = series[i];
|
||||
|
||||
// High should be >= max(Open, Close)
|
||||
Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
|
||||
|
||||
// Low should be <= min(Open, Close)
|
||||
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
|
||||
|
||||
// Volume should be positive
|
||||
Assert.True(bar.Volume > 0);
|
||||
|
||||
// All prices should be positive
|
||||
Assert.True(bar.Open > 0);
|
||||
Assert.True(bar.High > 0);
|
||||
Assert.True(bar.Low > 0);
|
||||
Assert.True(bar.Close > 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntraBarUpdates_ModifyCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
double initialClose = bar1.Close;
|
||||
|
||||
// Loop until price changes (random walk might stay same but unlikely)
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
Assert.Equal(initialTime, bar.Time);
|
||||
if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(changed, "Price should change during intra-bar updates");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedStreamingAndBatch_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Start with streaming
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// Batch generation with explicit time
|
||||
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.True(series[0].Time > bar2.Time);
|
||||
Assert.Equal(3, series.Count);
|
||||
|
||||
// Continue streaming after batch (uses internal state)
|
||||
var bar3 = gbm.Next();
|
||||
Assert.True(bar3.Time > series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriftAndVolatility_AffectPriceMovement()
|
||||
{
|
||||
// High volatility should produce more price variation
|
||||
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
|
||||
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
|
||||
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
|
||||
var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
|
||||
|
||||
// Calculate price ranges
|
||||
double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
|
||||
double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
|
||||
|
||||
// High volatility should generally produce larger absolute movements
|
||||
Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsecutiveCalls_MaintainContinuity()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// bar2.Open should equal bar1.Close (continuity)
|
||||
Assert.Equal(bar1.Close, bar2.Open);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stateless_NoHistoryStorage()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Generate multiple bars
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
gbm.Next();
|
||||
}
|
||||
|
||||
// GBM should not expose any history storage
|
||||
var type = gbm.GetType();
|
||||
var barsProperty = type.GetProperty("Bars");
|
||||
|
||||
Assert.Null(barsProperty);
|
||||
}
|
||||
}
|
||||
|
||||
+211
-211
@@ -1,211 +1,211 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Geometric Brownian Motion (GBM) generator for simulating OHLCV data.
|
||||
/// Generates realistic price data for testing indicators and strategies.
|
||||
/// Stateless design - only maintains minimal state needed for price continuity.
|
||||
/// </summary>
|
||||
public class GBM : IFeed
|
||||
{
|
||||
private readonly Random _rnd = new();
|
||||
|
||||
private double _lastPrice;
|
||||
private long _lastTime;
|
||||
|
||||
private readonly double _mu;
|
||||
private readonly double _sigma;
|
||||
private readonly double _dt;
|
||||
|
||||
// Precomputed GBM constants
|
||||
private readonly double _drift;
|
||||
private readonly double _vol;
|
||||
private readonly long _defaultTimeStep;
|
||||
|
||||
// State for streaming bar formation (only when isNew=false)
|
||||
private TBar _currentBar;
|
||||
private bool _hasCurrentBar;
|
||||
|
||||
// Box-Muller optimization: cache second normal
|
||||
private double _cachedZ;
|
||||
private bool _hasCachedZ;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GBM generator.
|
||||
/// </summary>
|
||||
/// <param name="startPrice">Initial price (default: 100.0)</param>
|
||||
/// <param name="mu">Annual drift/return rate (default: 0.05 = 5%)</param>
|
||||
/// <param name="sigma">Annual volatility (default: 0.2 = 20%)</param>
|
||||
/// <param name="defaultTimeframe">Default timeframe for bars (default: 1 minute)</param>
|
||||
public GBM(
|
||||
double startPrice = 100.0,
|
||||
double mu = 0.05,
|
||||
double sigma = 0.2,
|
||||
TimeSpan? defaultTimeframe = null)
|
||||
{
|
||||
_lastPrice = startPrice;
|
||||
_lastTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
_mu = mu;
|
||||
_sigma = sigma;
|
||||
|
||||
// Use provided timeframe or default to 1 minute
|
||||
var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1);
|
||||
_defaultTimeStep = timeframe.Ticks;
|
||||
|
||||
// Calculate dt based on timeframe (assuming 252 trading days/year, 6.5 hours/day)
|
||||
double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||
_dt = timeframe.TotalMinutes / minutesPerYear;
|
||||
|
||||
_drift = (mu - 0.5 * sigma * sigma) * _dt;
|
||||
_vol = sigma * Math.Sqrt(_dt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates next standard normal using Box-Muller transform with caching.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double NextNormal()
|
||||
{
|
||||
if (_hasCachedZ)
|
||||
{
|
||||
_hasCachedZ = false;
|
||||
return _cachedZ;
|
||||
}
|
||||
|
||||
double u1 = 1.0 - _rnd.NextDouble();
|
||||
double u2 = 1.0 - _rnd.NextDouble();
|
||||
double mag = Math.Sqrt(-2.0 * Math.Log(u1));
|
||||
double angle = 2.0 * Math.PI * u2;
|
||||
|
||||
_cachedZ = mag * Math.Sin(angle);
|
||||
_hasCachedZ = true;
|
||||
|
||||
return mag * Math.Cos(angle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with full bidirectional control.
|
||||
/// GBM always honors the request - isNew parameter unchanged on return.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(ref bool isNew)
|
||||
{
|
||||
// GBM always honors request - parameter unchanged
|
||||
|
||||
if (isNew || !_hasCurrentBar)
|
||||
{
|
||||
// Generate new bar
|
||||
long currentTime = _lastTime + _defaultTimeStep;
|
||||
|
||||
double z = NextNormal();
|
||||
double price = _lastPrice * Math.Exp(_drift + _vol * z);
|
||||
double volume = 1000 + _rnd.NextDouble() * 1000;
|
||||
|
||||
double open = _lastPrice;
|
||||
double close = price;
|
||||
double high = Math.Max(open, close) * (1.0 + _rnd.NextDouble() * 0.01);
|
||||
double low = Math.Min(open, close) * (1.0 - _rnd.NextDouble() * 0.01);
|
||||
|
||||
_currentBar = new TBar(currentTime, open, high, low, close, volume);
|
||||
_hasCurrentBar = true;
|
||||
|
||||
_lastPrice = close;
|
||||
_lastTime = currentTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update current bar (intra-bar tick)
|
||||
double z = NextNormal();
|
||||
double price = _lastPrice * Math.Exp(_drift + _vol * z);
|
||||
double volume = 1000 + _rnd.NextDouble() * 1000;
|
||||
|
||||
var bar = _currentBar;
|
||||
double newClose = price;
|
||||
double newHigh = Math.Max(bar.High, newClose);
|
||||
double newLow = Math.Min(bar.Low, newClose);
|
||||
|
||||
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, volume);
|
||||
_lastPrice = newClose;
|
||||
}
|
||||
|
||||
return _currentBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with simple control.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(bool isNew = true)
|
||||
{
|
||||
// Delegate to ref version
|
||||
return Next(ref isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
|
||||
{
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("Count must be positive", nameof(count));
|
||||
|
||||
var series = new TBarSeries(count);
|
||||
|
||||
// Pre-allocate arrays for SoA layout
|
||||
long[] t = new long[count];
|
||||
double[] o = new double[count];
|
||||
double[] h = new double[count];
|
||||
double[] l = new double[count];
|
||||
double[] c = new double[count];
|
||||
double[] v = new double[count];
|
||||
|
||||
// Calculate dt for this specific interval
|
||||
double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||
double dt = interval.TotalMinutes / minutesPerYear;
|
||||
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
|
||||
double vol = _sigma * Math.Sqrt(dt);
|
||||
|
||||
long timeStep = interval.Ticks;
|
||||
double currentPrice = _lastPrice;
|
||||
long currentTime = startTime;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double z = NextNormal();
|
||||
double price = currentPrice * Math.Exp(drift + vol * z);
|
||||
|
||||
double open = currentPrice;
|
||||
double close = price;
|
||||
|
||||
double rnd1 = _rnd.NextDouble();
|
||||
double rnd2 = _rnd.NextDouble();
|
||||
double rnd3 = _rnd.NextDouble();
|
||||
|
||||
t[i] = currentTime;
|
||||
o[i] = open;
|
||||
c[i] = close;
|
||||
h[i] = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
|
||||
l[i] = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
|
||||
v[i] = 1000 + rnd3 * 1000;
|
||||
|
||||
currentPrice = price;
|
||||
currentTime += timeStep;
|
||||
}
|
||||
|
||||
// Update internal state to continue from end of batch
|
||||
_lastPrice = currentPrice;
|
||||
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
|
||||
|
||||
// Bulk add to series
|
||||
series.Add(t, o, h, l, c, v);
|
||||
|
||||
// Reset streaming state after batch
|
||||
_hasCurrentBar = false;
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Geometric Brownian Motion (GBM) generator for simulating OHLCV data.
|
||||
/// Generates realistic price data for testing indicators and strategies.
|
||||
/// Stateless design - only maintains minimal state needed for price continuity.
|
||||
/// </summary>
|
||||
public class GBM : IFeed
|
||||
{
|
||||
private readonly Random _rnd = new();
|
||||
|
||||
private double _lastPrice;
|
||||
private long _lastTime;
|
||||
|
||||
private readonly double _mu;
|
||||
private readonly double _sigma;
|
||||
private readonly double _dt;
|
||||
|
||||
// Precomputed GBM constants
|
||||
private readonly double _drift;
|
||||
private readonly double _vol;
|
||||
private readonly long _defaultTimeStep;
|
||||
|
||||
// State for streaming bar formation (only when isNew=false)
|
||||
private TBar _currentBar;
|
||||
private bool _hasCurrentBar;
|
||||
|
||||
// Box-Muller optimization: cache second normal
|
||||
private double _cachedZ;
|
||||
private bool _hasCachedZ;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GBM generator.
|
||||
/// </summary>
|
||||
/// <param name="startPrice">Initial price (default: 100.0)</param>
|
||||
/// <param name="mu">Annual drift/return rate (default: 0.05 = 5%)</param>
|
||||
/// <param name="sigma">Annual volatility (default: 0.2 = 20%)</param>
|
||||
/// <param name="defaultTimeframe">Default timeframe for bars (default: 1 minute)</param>
|
||||
public GBM(
|
||||
double startPrice = 100.0,
|
||||
double mu = 0.05,
|
||||
double sigma = 0.2,
|
||||
TimeSpan? defaultTimeframe = null)
|
||||
{
|
||||
_lastPrice = startPrice;
|
||||
_lastTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
_mu = mu;
|
||||
_sigma = sigma;
|
||||
|
||||
// Use provided timeframe or default to 1 minute
|
||||
var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1);
|
||||
_defaultTimeStep = timeframe.Ticks;
|
||||
|
||||
// Calculate dt based on timeframe (assuming 252 trading days/year, 6.5 hours/day)
|
||||
double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||
_dt = timeframe.TotalMinutes / minutesPerYear;
|
||||
|
||||
_drift = (mu - 0.5 * sigma * sigma) * _dt;
|
||||
_vol = sigma * Math.Sqrt(_dt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates next standard normal using Box-Muller transform with caching.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double NextNormal()
|
||||
{
|
||||
if (_hasCachedZ)
|
||||
{
|
||||
_hasCachedZ = false;
|
||||
return _cachedZ;
|
||||
}
|
||||
|
||||
double u1 = 1.0 - _rnd.NextDouble();
|
||||
double u2 = 1.0 - _rnd.NextDouble();
|
||||
double mag = Math.Sqrt(-2.0 * Math.Log(u1));
|
||||
double angle = 2.0 * Math.PI * u2;
|
||||
|
||||
_cachedZ = mag * Math.Sin(angle);
|
||||
_hasCachedZ = true;
|
||||
|
||||
return mag * Math.Cos(angle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with full bidirectional control.
|
||||
/// GBM always honors the request - isNew parameter unchanged on return.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(ref bool isNew)
|
||||
{
|
||||
// GBM always honors request - parameter unchanged
|
||||
|
||||
if (isNew || !_hasCurrentBar)
|
||||
{
|
||||
// Generate new bar
|
||||
long currentTime = _lastTime + _defaultTimeStep;
|
||||
|
||||
double z = NextNormal();
|
||||
double price = _lastPrice * Math.Exp(_drift + _vol * z);
|
||||
double volume = 1000 + _rnd.NextDouble() * 1000;
|
||||
|
||||
double open = _lastPrice;
|
||||
double close = price;
|
||||
double high = Math.Max(open, close) * (1.0 + _rnd.NextDouble() * 0.01);
|
||||
double low = Math.Min(open, close) * (1.0 - _rnd.NextDouble() * 0.01);
|
||||
|
||||
_currentBar = new TBar(currentTime, open, high, low, close, volume);
|
||||
_hasCurrentBar = true;
|
||||
|
||||
_lastPrice = close;
|
||||
_lastTime = currentTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update current bar (intra-bar tick)
|
||||
double z = NextNormal();
|
||||
double price = _lastPrice * Math.Exp(_drift + _vol * z);
|
||||
double volume = 1000 + _rnd.NextDouble() * 1000;
|
||||
|
||||
var bar = _currentBar;
|
||||
double newClose = price;
|
||||
double newHigh = Math.Max(bar.High, newClose);
|
||||
double newLow = Math.Min(bar.Low, newClose);
|
||||
|
||||
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, volume);
|
||||
_lastPrice = newClose;
|
||||
}
|
||||
|
||||
return _currentBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with simple control.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(bool isNew = true)
|
||||
{
|
||||
// Delegate to ref version
|
||||
return Next(ref isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
|
||||
{
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("Count must be positive", nameof(count));
|
||||
|
||||
var series = new TBarSeries(count);
|
||||
|
||||
// Pre-allocate arrays for SoA layout
|
||||
long[] t = new long[count];
|
||||
double[] o = new double[count];
|
||||
double[] h = new double[count];
|
||||
double[] l = new double[count];
|
||||
double[] c = new double[count];
|
||||
double[] v = new double[count];
|
||||
|
||||
// Calculate dt for this specific interval
|
||||
double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||
double dt = interval.TotalMinutes / minutesPerYear;
|
||||
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
|
||||
double vol = _sigma * Math.Sqrt(dt);
|
||||
|
||||
long timeStep = interval.Ticks;
|
||||
double currentPrice = _lastPrice;
|
||||
long currentTime = startTime;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double z = NextNormal();
|
||||
double price = currentPrice * Math.Exp(drift + vol * z);
|
||||
|
||||
double open = currentPrice;
|
||||
double close = price;
|
||||
|
||||
double rnd1 = _rnd.NextDouble();
|
||||
double rnd2 = _rnd.NextDouble();
|
||||
double rnd3 = _rnd.NextDouble();
|
||||
|
||||
t[i] = currentTime;
|
||||
o[i] = open;
|
||||
c[i] = close;
|
||||
h[i] = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
|
||||
l[i] = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
|
||||
v[i] = 1000 + rnd3 * 1000;
|
||||
|
||||
currentPrice = price;
|
||||
currentTime += timeStep;
|
||||
}
|
||||
|
||||
// Update internal state to continue from end of batch
|
||||
_lastPrice = currentPrice;
|
||||
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
|
||||
|
||||
// Bulk add to series
|
||||
series.Add(t, o, h, l, c, v);
|
||||
|
||||
// Reset streaming state after batch
|
||||
_hasCurrentBar = false;
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user