Trading: components 161-180
This commit is contained in:
105 files changed
+6042
-1
No files matched your search
+222
@@ -0,0 +1,222 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// OrderType is limit, market, stop or stop limit.
|
||||
type OrderType int
|
||||
|
||||
const (
|
||||
OrderLimit OrderType = iota
|
||||
OrderMarket
|
||||
OrderStop
|
||||
OrderStopLimit
|
||||
)
|
||||
|
||||
// TimeInForce is how long an order rests.
|
||||
type TimeInForce int
|
||||
|
||||
const (
|
||||
TIFDay TimeInForce = iota
|
||||
TIFGoodTillCanceled
|
||||
TIFImmediateOrCancel
|
||||
TIFFillOrKill
|
||||
)
|
||||
|
||||
// OrderDraft is an order as entered.
|
||||
type OrderDraft struct {
|
||||
Side OrderSide
|
||||
Type OrderType
|
||||
Quantity float64
|
||||
Price float64
|
||||
Trigger float64
|
||||
TIF TimeInForce
|
||||
}
|
||||
|
||||
// OrderEntryOptions configure an OrderEntry. Symbol is required.
|
||||
type OrderEntryOptions struct {
|
||||
Symbol string
|
||||
// Last is the last price; zero means unknown.
|
||||
Last float64
|
||||
// TickSize and LotSize step prices and quantities; zero: 0.01, 1.
|
||||
TickSize, LotSize float64
|
||||
// MaxQuantity caps the quantity; zero means no cap.
|
||||
MaxQuantity float64
|
||||
// Initial is what Reset restores; a zero Side means buy.
|
||||
Initial OrderDraft
|
||||
// Validate adds the caller's rules, returning a message or "".
|
||||
Validate func(o OrderDraft) string
|
||||
// Check starts an async check, finished with OrderEntryState.Resolve.
|
||||
Check func(o OrderDraft, version uint64)
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// OrderEntryState holds the draft and its form state.
|
||||
type OrderEntryState struct {
|
||||
Draft OrderDraft
|
||||
Form FormState
|
||||
started bool
|
||||
}
|
||||
|
||||
// Resolve delivers Check's result; stale versions return false.
|
||||
func (s *OrderEntryState) Resolve(version uint64, err string) bool {
|
||||
return s.Form.Resolve("quantity", version, err)
|
||||
}
|
||||
|
||||
// EndSubmit finishes a submit; a formError shows, keeping the draft.
|
||||
func (s *OrderEntryState) EndSubmit(formError string) { s.Form.EndSubmit(formError) }
|
||||
|
||||
// OrderEntryResult is a built OrderEntry and its submitted order.
|
||||
type OrderEntryResult struct {
|
||||
Element *ui.Element
|
||||
order OrderDraft
|
||||
submitted bool
|
||||
}
|
||||
|
||||
// Submitted returns the checked order; call EndSubmit once placed.
|
||||
func (r OrderEntryResult) Submitted() (OrderDraft, bool) { return r.order, r.submitted }
|
||||
|
||||
// OrderEntry edits an order and submits it once checked.
|
||||
func OrderEntry(c *ui.Context, s *OrderEntryState, opts OrderEntryOptions) OrderEntryResult {
|
||||
if opts.Symbol == "" {
|
||||
panic("mujica: OrderEntry needs a Symbol")
|
||||
}
|
||||
if opts.TickSize < 0 || opts.LotSize < 0 || opts.MaxQuantity < 0 || opts.Last < 0 {
|
||||
panic("mujica: OrderEntry TickSize, LotSize, MaxQuantity and Last cannot be negative")
|
||||
}
|
||||
tick, lot := opts.TickSize, opts.LotSize
|
||||
if tick == 0 {
|
||||
tick = 0.01
|
||||
}
|
||||
if lot == 0 {
|
||||
lot = 1
|
||||
}
|
||||
initial := opts.Initial
|
||||
if initial.Side == 0 {
|
||||
initial.Side = SideBuy
|
||||
}
|
||||
if !s.started {
|
||||
s.started, s.Draft = true, initial
|
||||
}
|
||||
priceSpec := numberInputResolve(NumberInputOptions{Step: tick})
|
||||
k := Tokens(c)
|
||||
u := Density(c).Unit()
|
||||
var r OrderEntryResult
|
||||
r.Element = ui.Column(c).Gap(u * 4).MaxWidth(360).Disabled(opts.Disabled).Role(ui.RoleGroup).Label(msg(c, "order_entry.label"))
|
||||
disabled := r.Element.IsDisabled()
|
||||
r.Element.Children(func() {
|
||||
side := 0
|
||||
if s.Draft.Side == SideSell {
|
||||
side = 1
|
||||
}
|
||||
sides := []Segment{{Label: msg(c, "order_entry.buy")}, {Label: msg(c, "order_entry.sell")}}
|
||||
SegmentedControl(c, &side, sides, SegmentedControlOptions{Equal: true, Label: msg(c, "order_entry.side"), Disabled: disabled})
|
||||
s.Draft.Side = SideBuy + OrderSide(side)
|
||||
var types []SelectOption[OrderType]
|
||||
for t := OrderLimit; t <= OrderStopLimit; t++ {
|
||||
types = append(types, SelectOption[OrderType]{Value: t, Label: msg(c, fmt.Sprintf("order_entry.type%d", t))})
|
||||
}
|
||||
ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() {
|
||||
ui.Text(c, opts.Symbol).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).Grow(1)
|
||||
Select(c, &s.Draft.Type, types, SelectOptions{Label: msg(c, "order_entry.type"), Disabled: disabled})
|
||||
})
|
||||
submit := fmt.Sprintf(msg(c, fmt.Sprintf("order_entry.submit%d", s.Draft.Side)), opts.Symbol)
|
||||
res := Form(c, &s.Form, FormOptions{SubmitLabel: submit}, func(f *FormScope) {
|
||||
orderEntryFields(c, f, s, opts, initial, tick, lot, disabled)
|
||||
})
|
||||
ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() {
|
||||
ui.Text(c, msg(c, "order_entry.estimate")).TextColor(k.TextMuted)
|
||||
est := "—"
|
||||
if p := orderEntryPrice(s.Draft, opts.Last); p > 0 && s.Draft.Quantity > 0 {
|
||||
est = "≈ " + statisticFormat(p*s.Draft.Quantity, priceSpec.decimals)
|
||||
}
|
||||
ui.Text(c, est).FontFeatures("tnum").FontWeight(600)
|
||||
})
|
||||
if res.Submitted() {
|
||||
r.order, r.submitted = s.Draft, true
|
||||
slog.Debug("mujica: order_entry submitted", "side", s.Draft.Side, "type", s.Draft.Type)
|
||||
}
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// orderEntryPrice is the expected fill price, or 0 if unknown.
|
||||
func orderEntryPrice(o OrderDraft, last float64) float64 {
|
||||
switch o.Type {
|
||||
case OrderLimit, OrderStopLimit:
|
||||
return o.Price
|
||||
case OrderStop:
|
||||
return o.Trigger
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
// orderEntryFields registers the fields the order type needs.
|
||||
func orderEntryFields(c *ui.Context, f *FormScope, s *OrderEntryState, opts OrderEntryOptions, initial OrderDraft, tick, lot float64, disabled bool) {
|
||||
maxQ := opts.MaxQuantity
|
||||
FormItem(c, f, "quantity", &s.Draft, FormItemOptions[OrderDraft]{Label: msg(c, "order_entry.quantity"), Required: true, Initial: initial,
|
||||
Validate: func(o OrderDraft) string { return orderEntryCheck(c, o, opts, maxQ) },
|
||||
Async: opts.Check,
|
||||
}, func() *ui.Element {
|
||||
return NumberInput(c, &s.Draft.Quantity, NumberInputOptions{Step: lot, Disabled: disabled, Error: s.Form.Error("quantity")}).Input
|
||||
})
|
||||
if s.Draft.Type == OrderStop || s.Draft.Type == OrderStopLimit {
|
||||
FormItem(c, f, "trigger", &s.Draft.Trigger, FormItemOptions[float64]{Label: msg(c, "order_entry.trigger"), Required: true, Initial: initial.Trigger,
|
||||
Validate: func(v float64) string { return orderEntryTrigger(c, s.Draft, opts.Last) },
|
||||
}, func() *ui.Element {
|
||||
return NumberInput(c, &s.Draft.Trigger, NumberInputOptions{Step: tick, Disabled: disabled, Error: s.Form.Error("trigger")}).Input
|
||||
})
|
||||
}
|
||||
if s.Draft.Type == OrderLimit || s.Draft.Type == OrderStopLimit {
|
||||
FormItem(c, f, "price", &s.Draft.Price, FormItemOptions[float64]{Label: msg(c, "order_entry.price"), Required: true, Initial: initial.Price,
|
||||
Validate: func(v float64) string {
|
||||
if v <= 0 {
|
||||
return msg(c, "order_entry.price_positive")
|
||||
}
|
||||
return ""
|
||||
},
|
||||
}, func() *ui.Element {
|
||||
return NumberInput(c, &s.Draft.Price, NumberInputOptions{Step: tick, Disabled: disabled, Error: s.Form.Error("price")}).Input
|
||||
})
|
||||
}
|
||||
var tifs []SelectOption[TimeInForce]
|
||||
for t := TIFDay; t <= TIFFillOrKill; t++ {
|
||||
tifs = append(tifs, SelectOption[TimeInForce]{Value: t, Label: msg(c, fmt.Sprintf("order_entry.tif%d", t))})
|
||||
}
|
||||
FormItem(c, f, "tif", &s.Draft.TIF, FormItemOptions[TimeInForce]{Label: msg(c, "order_entry.tif"), Initial: initial.TIF}, func() *ui.Element {
|
||||
return Select(c, &s.Draft.TIF, tifs, SelectOptions{Label: msg(c, "order_entry.tif"), Disabled: disabled}).Element
|
||||
})
|
||||
}
|
||||
|
||||
// orderEntryCheck checks the quantity, then the caller's rules.
|
||||
func orderEntryCheck(c *ui.Context, o OrderDraft, opts OrderEntryOptions, maxQ float64) string {
|
||||
switch {
|
||||
case o.Quantity <= 0:
|
||||
return msg(c, "order_entry.quantity_positive")
|
||||
case maxQ > 0 && o.Quantity > maxQ:
|
||||
return fmt.Sprintf(msg(c, "order_entry.quantity_max"), statisticFormat(maxQ, 0))
|
||||
}
|
||||
if opts.Validate != nil {
|
||||
return opts.Validate(o)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// orderEntryTrigger checks a stop's side of the last price.
|
||||
func orderEntryTrigger(c *ui.Context, o OrderDraft, last float64) string {
|
||||
switch {
|
||||
case o.Trigger <= 0:
|
||||
return msg(c, "order_entry.trigger_positive")
|
||||
case last > 0 && o.Side == SideBuy && o.Trigger <= last:
|
||||
return msg(c, "order_entry.trigger_above")
|
||||
case last > 0 && o.Side == SideSell && o.Trigger >= last:
|
||||
return msg(c, "order_entry.trigger_below")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in new issue
Block a user