diff --git a/.gitignore b/.gitignore index 3b735ec..ee09abe 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ *.dll *.so *.dylib +indicator-backtest +indicator-sync # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index 3f9b7b7..034382a 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,12 @@ The [Sync function]() facilitates the synchronization of assets between designat The `indicator-sync` command line tool also offers the capability of synchronizing data between the Tiingo Repository and the File System Repository. To illustrate its usage, consider the following example command: ```bash -$ indicator-sync -key $TIINGO_KEY -target /home/user/assets -days 30 +$ indicator-sync \ + -source-name tiingo \ + -source-config $TIINGO_KEY \ + -target-name filesystem \ + -target-config /home/user/assets \ + -days 30 ``` This command effectively retrieves the most recent snapshots for assets residing within the `/home/user/assets` directory from the Tiingo Repository. In the event that the local asset file is devoid of content, it automatically extends its reach to synchronize 30 days' worth of snapshots, ensuring a comprehensive and up-to-date repository. @@ -201,7 +206,11 @@ if err != nil { The `indicator-backtest` command line tool empowers users to conduct comprehensive backtesting of assets residing within a specified repository. This capability encompasses the application of all currently recognized strategies, culminating in the generation of detailed reports within a designated output directory. ```bash -$ indicator-backtest -repository /home/user/assets -output /home/user/reports -workers 1 +$ indicator-backtest \ + -source-name filesystem \ + -source-config /home/user/assets \ + -output /home/user/reports \ + -workers 1 ``` Usage diff --git a/asset/README.md b/asset/README.md index 0c18aab..07e8c9b 100644 --- a/asset/README.md +++ b/asset/README.md @@ -26,6 +26,7 @@ The information provided on this project is strictly for informational purposes - [Constants](<#constants>) - [Variables](<#variables>) +- [func RegisterRepositoryBuilder\(name string, builder RepositoryBuilderFunc\)](<#RegisterRepositoryBuilder>) - [func SnapshotsAsClosings\(snapshots \<\-chan \*Snapshot\) \<\-chan float64](<#SnapshotsAsClosings>) - [func SnapshotsAsDates\(snapshots \<\-chan \*Snapshot\) \<\-chan time.Time](<#SnapshotsAsDates>) - [func SnapshotsAsHighs\(snapshots \<\-chan \*Snapshot\) \<\-chan float64](<#SnapshotsAsHighs>) @@ -47,6 +48,8 @@ The information provided on this project is strictly for informational purposes - [func \(r \*InMemoryRepository\) GetSince\(name string, date time.Time\) \(\<\-chan \*Snapshot, error\)](<#InMemoryRepository.GetSince>) - [func \(r \*InMemoryRepository\) LastDate\(name string\) \(time.Time, error\)](<#InMemoryRepository.LastDate>) - [type Repository](<#Repository>) + - [func NewRepository\(name, config string\) \(Repository, error\)](<#NewRepository>) +- [type RepositoryBuilderFunc](<#RepositoryBuilderFunc>) - [type Snapshot](<#Snapshot>) - [type Sync](<#Sync>) - [func NewSync\(\) \*Sync](<#NewSync>) @@ -65,6 +68,21 @@ The information provided on this project is strictly for informational purposes ## Constants + + +```go +const ( + // InMemoryRepositoryBuilderName is the name for the in memory repository builder. + InMemoryRepositoryBuilderName = "memory" + + // FileSystemRepositoryBuilderName is the name for the file system repository builder. + FileSystemRepositoryBuilderName = "filesystem" + + // TiingoRepositoryBuilderName is the name of the Tiingo repository builder. + TiingoRepositoryBuilderName = "tiingo" +) +``` + ```go @@ -91,6 +109,15 @@ var ErrRepositoryAssetEmpty = errors.New("asset empty") var ErrRepositoryAssetNotFound = errors.New("asset is not found") ``` + +## func [RegisterRepositoryBuilder]() + +```go +func RegisterRepositoryBuilder(name string, builder RepositoryBuilderFunc) +``` + +RegisterRepositoryBuilder registers the given builder. + ## func [SnapshotsAsClosings]() @@ -303,6 +330,24 @@ type Repository interface { } ``` + +### func [NewRepository]() + +```go +func NewRepository(name, config string) (Repository, error) +``` + +NewRepository builds a new repository by the given name type and the configuration. + + +## type [RepositoryBuilderFunc]() + +RepositoryBuilderFunc defines a function to build a new repository using the given configuration parameter. + +```go +type RepositoryBuilderFunc func(config string) (Repository, error) +``` + ## type [Snapshot]() diff --git a/asset/repository_factory.go b/asset/repository_factory.go new file mode 100644 index 0000000..0dc9d67 --- /dev/null +++ b/asset/repository_factory.go @@ -0,0 +1,60 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package asset + +import ( + "fmt" +) + +const ( + // InMemoryRepositoryBuilderName is the name for the in memory repository builder. + InMemoryRepositoryBuilderName = "memory" + + // FileSystemRepositoryBuilderName is the name for the file system repository builder. + FileSystemRepositoryBuilderName = "filesystem" + + // TiingoRepositoryBuilderName is the name of the Tiingo repository builder. + TiingoRepositoryBuilderName = "tiingo" +) + +// RepositoryBuilderFunc defines a function to build a new repository using the given configuration parameter. +type RepositoryBuilderFunc func(config string) (Repository, error) + +// repositoryBuilders provides mapping for the repository builders. +var repositoryBuilders = map[string]RepositoryBuilderFunc{ + InMemoryRepositoryBuilderName: inMemoryRepositoryBuilder, + FileSystemRepositoryBuilderName: fileSystemRepositoryBuilder, + TiingoRepositoryBuilderName: tiingoRepositoryBuilder, +} + +// RegisterRepositoryBuilder registers the given builder. +func RegisterRepositoryBuilder(name string, builder RepositoryBuilderFunc) { + repositoryBuilders[name] = builder +} + +// NewRepository builds a new repository by the given name type and the configuration. +func NewRepository(name, config string) (Repository, error) { + builder, ok := repositoryBuilders[name] + if !ok { + return nil, fmt.Errorf("unknown repository: %s", name) + } + + return builder(config) +} + +// inMemoryRepositoryBuilder builds a new in memory repository instance. +func inMemoryRepositoryBuilder(_ string) (Repository, error) { + return NewInMemoryRepository(), nil +} + +// fileSystemRepositoryBuilder builds a new file system repository instance. +func fileSystemRepositoryBuilder(config string) (Repository, error) { + return NewFileSystemRepository(config), nil +} + +// tiingoRepositoryBuilder builds a new Tiingo repository instance. +func tiingoRepositoryBuilder(config string) (Repository, error) { + return NewTiingoRepository(config), nil +} diff --git a/asset/repository_factory_test.go b/asset/repository_factory_test.go new file mode 100644 index 0000000..4625a02 --- /dev/null +++ b/asset/repository_factory_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2021-2024 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package asset_test + +import ( + "testing" + + "github.com/cinar/indicator/v2/asset" +) + +func TestNewRepositoryUnknown(t *testing.T) { + repository, err := asset.NewRepository("unknown", "") + if err == nil { + t.Fatalf("unknown repository: %T", repository) + } +} + +func TestRegisterRepositoryBuilder(t *testing.T) { + builderName := "testbuilder" + + repository, err := asset.NewRepository(builderName, "") + if err == nil { + t.Fatalf("testbuilder is: %T", repository) + } + + asset.RegisterRepositoryBuilder(builderName, func(_ string) (asset.Repository, error) { + return asset.NewInMemoryRepository(), nil + }) + + repository, err = asset.NewRepository(builderName, "") + if err != nil { + t.Fatalf("testbuilder is not found: %v", err) + } + + _, ok := repository.(*asset.InMemoryRepository) + if !ok { + t.Fatalf("testbuilder is: %T", repository) + } +} + +func TestNewRepositoryMemory(t *testing.T) { + repository, err := asset.NewRepository(asset.InMemoryRepositoryBuilderName, "") + if err != nil { + t.Fatal(err) + } + + _, ok := repository.(*asset.InMemoryRepository) + if !ok { + t.Fatalf("repository not correct type: %T", repository) + } +} + +func TestNewRepositoryFileSystem(t *testing.T) { + repository, err := asset.NewRepository(asset.FileSystemRepositoryBuilderName, "testdata") + if err != nil { + t.Fatal(err) + } + + _, ok := repository.(*asset.FileSystemRepository) + if !ok { + t.Fatalf("repository not correct type: %T", repository) + } +} + +func TestNewTiingoRepository(t *testing.T) { + repository, err := asset.NewRepository(asset.TiingoRepositoryBuilderName, "1234") + if err != nil { + t.Fatal(err) + } + + _, ok := repository.(*asset.TiingoRepository) + if !ok { + t.Fatalf("repository not correct type: %T", repository) + } +} diff --git a/cmd/indicator-backtest/main.go b/cmd/indicator-backtest/main.go index 9837c7e..e8ae154 100644 --- a/cmd/indicator-backtest/main.go +++ b/cmd/indicator-backtest/main.go @@ -21,7 +21,8 @@ import ( ) func main() { - var repositoryDir string + var sourceName string + var sourceConfig string var outputDir string var workers int var lastDays int @@ -36,7 +37,8 @@ func main() { fmt.Fprintln(os.Stderr, "https://github.com/cinar/indicator") fmt.Fprintln(os.Stderr) - flag.StringVar(&repositoryDir, "repository", ".", "file system repository directory") + flag.StringVar(&sourceName, "source-name", "filesystem", "source repository type") + flag.StringVar(&sourceConfig, "source-config", "", "source repository config") flag.StringVar(&outputDir, "output", ".", "output directory") flag.IntVar(&workers, "workers", strategy.DefaultBacktestWorkers, "number of concurrent workers") flag.IntVar(&lastDays, "last", strategy.DefaultLastDays, "number of days to do backtest") @@ -46,11 +48,12 @@ func main() { flag.StringVar(&dateFormat, "date-format", helper.DefaultReportDateFormat, "date format to use") flag.Parse() - flag.Parse() - - repository := asset.NewFileSystemRepository(repositoryDir) + source, err := asset.NewRepository(sourceName, sourceConfig) + if err != nil { + log.Fatalf("unable to initialize source: %v", err) + } - backtest := strategy.NewBacktest(repository, outputDir) + backtest := strategy.NewBacktest(source, outputDir) backtest.Workers = workers backtest.LastDays = lastDays backtest.WriteStrategyReports = writeStrategyRerpots @@ -70,8 +73,8 @@ func main() { backtest.Strategies = append(backtest.Strategies, strategy.AllAndStrategies(backtest.Strategies)...) } - err := backtest.Run() + err = backtest.Run() if err != nil { - log.Fatal(err) + log.Fatalf("unable to run backtest: %v", err) } } diff --git a/cmd/indicator-sync/main.go b/cmd/indicator-sync/main.go index 95da46f..d57a8d4 100644 --- a/cmd/indicator-sync/main.go +++ b/cmd/indicator-sync/main.go @@ -16,8 +16,10 @@ import ( ) func main() { - var tiingoKey string - var targetBase string + var sourceName string + var sourceConfig string + var targetName string + var targetConfig string var minusDays int var workers int var delay int @@ -28,29 +30,42 @@ func main() { fmt.Fprintln(os.Stderr, "https://github.com/cinar/indicator") fmt.Fprintln(os.Stderr) - flag.StringVar(&tiingoKey, "key", "", "tiingo service api key") - flag.StringVar(&targetBase, "target", ".", "target repository base directory") + flag.StringVar(&sourceName, "source-name", "tiingo", "source repository type") + flag.StringVar(&sourceConfig, "source-config", "", "source repository config") + flag.StringVar(&targetName, "target-name", "filesystem", "target repository type") + flag.StringVar(&targetConfig, "target-config", "", "target repository config") flag.IntVar(&minusDays, "days", 0, "lookback period in days for the new assets") flag.IntVar(&workers, "workers", asset.DefaultSyncWorkers, "number of concurrent workers") flag.IntVar(&delay, "delay", asset.DefaultSyncDelay, "delay between each get") flag.Parse() - if tiingoKey == "" { - log.Fatal("Tiingo API key required") + source, err := asset.NewRepository(sourceName, sourceConfig) + if err != nil { + log.Fatalf("unable to initialize source: %v", err) + } + + target, err := asset.NewRepository(targetName, targetConfig) + if err != nil { + log.Fatalf("unable to initialize target: %v", err) } defaultStartDate := time.Now().AddDate(0, 0, -minusDays) - source := asset.NewTiingoRepository(tiingoKey) - target := asset.NewFileSystemRepository(targetBase) + assets := flag.Args() + if len(assets) == 0 { + assets, err = source.Assets() + if err != nil { + log.Fatalf("unable to get assets: %v", err) + } + } sync := asset.NewSync() sync.Workers = workers sync.Delay = delay - sync.Assets = flag.Args() + sync.Assets = assets - err := sync.Run(source, target, defaultStartDate) + err = sync.Run(source, target, defaultStartDate) if err != nil { - log.Fatal(err) + log.Fatalf("unable to sync repositories: %v", err) } } diff --git a/helper/skip.go b/helper/skip.go index 69dcbd5..bec5f90 100644 --- a/helper/skip.go +++ b/helper/skip.go @@ -17,7 +17,10 @@ func Skip[T any](c <-chan T, count int) <-chan T { go func() { for i := 0; i < count; i++ { - <-c + _, ok := <-c + if !ok { + break + } } Pipe(c, result) diff --git a/strategy/README.md b/strategy/README.md index b0a9ef0..c4038bd 100644 --- a/strategy/README.md +++ b/strategy/README.md @@ -89,7 +89,7 @@ const ( ``` -## func [ActionSources]() +## func [ActionSources]() ```go func ActionSources(strategies []Strategy, snapshots <-chan *asset.Snapshot) []<-chan Action @@ -555,7 +555,7 @@ func AllSplitStrategies(strategies []Strategy) []Strategy AllSplitStrategies performs a cartesian product operation on the given strategies, resulting in a collection containing all split strategies formed by combining individual buy and sell strategies. -### func [AllStrategies]() +### func [AllStrategies]() ```go func AllStrategies() []Strategy diff --git a/strategy/strategy.go b/strategy/strategy.go index cb26887..d4257b5 100644 --- a/strategy/strategy.go +++ b/strategy/strategy.go @@ -43,16 +43,11 @@ func ComputeWithOutcome(s Strategy, c <-chan *asset.Snapshot) (<-chan Action, <- snapshots := helper.Duplicate(c, 2) actions := helper.Duplicate(s.Compute(snapshots[0]), 2) + closings := asset.SnapshotsAsClosings(snapshots[1]) - openings := helper.Skip(asset.SnapshotsAsOpenings(snapshots[1]), 1) + outcomes := Outcome(closings, actions[1]) - outcomes := helper.Echo( - Outcome(openings, actions[0]), - 1, - 1, - ) - - return actions[1], outcomes + return actions[0], outcomes } // AllStrategies returns a slice containing references to all available base strategies. diff --git a/taskfile.yml b/taskfile.yml index 9ace79e..ae2fad0 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -30,3 +30,8 @@ tasks: docs: cmds: - go run github.com/princjef/gomarkdoc/cmd/gomarkdoc@v1.1.0 ./... + + build-tools: + cmds: + - go build -o indicator-backtest cmd/indicator-backtest/main.go + - go build -o indicator-sync cmd/indicator-sync/main.go