Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions cmd/trader/brokerservice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"testing"

"github.com/rustyeddy/trader/instrument"
"github.com/rustyeddy/trader/num"
"github.com/rustyeddy/trader/order"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestNoPriceSource proves noPriceSource's own contract directly: it
// reports a real ModelInfo (so a formatter that inspects it never sees
// a zero value) but always refuses to price anything, since the
// accounts/snapshot commands it backs never call Submit and so should
// never actually reach FillPriceSource.Price in normal operation.
func TestNoPriceSource(t *testing.T) {
src := noPriceSource{}
assert.Equal(t, "none", src.Info().Name)

_, err := src.Price(mustSimListing(t), order.Buy)
require.ErrorContains(t, err, "no fill price source configured")
}

// TestCliPriceSource proves cliPriceSource only prices the one symbol
// it was configured for -- a submit against a different listing (which
// should never happen in practice, since buildSimListing and
// cliPriceSource are built from the same --symbol flag, but the type
// itself must still fail closed rather than silently pricing the wrong
// instrument) is rejected rather than silently using the configured
// price.
func TestCliPriceSource(t *testing.T) {
src := cliPriceSource{symbol: "EURUSD", price: mustPrice(t, "1.10000")}
assert.Contains(t, src.Info().Config, "EURUSD")

price, err := src.Price(mustSimListing(t), order.Buy)
require.NoError(t, err)
assert.True(t, price.Equal(mustPrice(t, "1.10000")))

other := mustSimListingWithSymbol(t, "GBPUSD")
_, err = src.Price(other, order.Sell)
require.ErrorContains(t, err, "no price configured for GBPUSD")
}

func TestResolveSubmitPriceSource(t *testing.T) {
t.Run("market order requires a price", func(t *testing.T) {
_, err := resolveSubmitPriceSource(order.Market, "EURUSD", "")
require.ErrorContains(t, err, "--price is required")
})

t.Run("market order with a price builds a cliPriceSource", func(t *testing.T) {
src, err := resolveSubmitPriceSource(order.Market, "EURUSD", "1.10000")
require.NoError(t, err)
require.IsType(t, cliPriceSource{}, src)
})

t.Run("market order with an invalid price is rejected", func(t *testing.T) {
_, err := resolveSubmitPriceSource(order.Market, "EURUSD", "not-a-number")
require.ErrorContains(t, err, "--price")
})

t.Run("non-market order needs no price and uses noPriceSource", func(t *testing.T) {
src, err := resolveSubmitPriceSource(order.Limit, "EURUSD", "")
require.NoError(t, err)
require.IsType(t, noPriceSource{}, src)
})
}

func mustSimListing(t *testing.T) instrument.Listing {
t.Helper()
return mustSimListingWithSymbol(t, "EURUSD")
}

func mustSimListingWithSymbol(t *testing.T, symbol string) instrument.Listing {
t.Helper()
listing, err := buildSimListing(simListingFlags{
symbol: symbol,
tickSize: "0.00001",
quantityIncrement: "1",
multiplier: "1",
}, "sim")
require.NoError(t, err)
return listing
}

func mustPrice(t *testing.T, s string) num.Price {
t.Helper()
p, err := num.ParsePrice(s)
require.NoError(t, err)
return p
}
35 changes: 35 additions & 0 deletions cmd/trader/brokervertical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -59,6 +60,31 @@ func TestBrokerVerticalSlice(t *testing.T) {
require.Empty(t, decoded.Positions)
})

t.Run("accounts renders as JSON", func(t *testing.T) {
out, err := runBroker(t, "accounts",
"--starting-cash", "10000", "--currency", "USD", "--format", "json")
require.NoError(t, err)

var decoded struct {
Accounts []struct {
AccountID string `json:"account_id"`
Broker string `json:"broker"`
} `json:"accounts"`
}
require.NoError(t, json.Unmarshal([]byte(out), &decoded))
require.Len(t, decoded.Accounts, 1)
assert.Equal(t, "sim", decoded.Accounts[0].Broker)
})

t.Run("snapshot renders as a table", func(t *testing.T) {
out, err := runBroker(t, "snapshot",
"--starting-cash", "10000", "--currency", "USD", "--format", "table")
require.NoError(t, err)
assert.Contains(t, out, "equity=10000 USD")
assert.Contains(t, out, "positions: (none)")
assert.Contains(t, out, "open orders: (none)")
})

t.Run("submit fills a market order and reports it filled", func(t *testing.T) {
out, err := runBroker(t, "submit",
"--symbol", "EURUSD", "--side", "buy", "--quantity", "1000",
Expand All @@ -74,6 +100,15 @@ func TestBrokerVerticalSlice(t *testing.T) {
require.Equal(t, "1000", decoded.FilledQty)
})

t.Run("submit renders a filled market order as a table", func(t *testing.T) {
out, err := runBroker(t, "submit",
"--symbol", "EURUSD", "--side", "buy", "--quantity", "1000",
"--price", "1.10000", "--format", "table")
require.NoError(t, err)
assert.Contains(t, out, "status=filled")
assert.Contains(t, out, "symbol=EURUSD side=buy filled_qty=1000")
})

t.Run("submit accepts a limit order as working, without filling it", func(t *testing.T) {
out, err := runBroker(t, "submit",
"--symbol", "EURUSD", "--side", "buy", "--type", "limit",
Expand Down
Loading
Loading