package Finance::GDAX::API::Order;
our $VERSION = '0.03';
use 5.20.0;
use warnings;
use Moose;
use Finance::GDAX::API::TypeConstraints;;
use Finance::GDAX::API;
use namespace::autoclean;

extends 'Finance::GDAX::API';

our @body_attributes = ('client_oid',
			'type',
			'side',
			'product_id',
			'stp',
			'price',
			'size',
			'time_in_force',
			'cancel_after',
			'post_only',
			'funds',
			'overdraft_enabled',
			'funding_amount');

## Common t all order types
#  
has 'client_oid' => (is  => 'rw',
		     isa => 'Str',
    );
has 'type' => (is  => 'rw',
	       isa => 'OrderType',
	       default => 'limit',
    );
has 'side' => (is  => 'rw',
	       isa => 'OrderSide',
    );
has 'product_id' => (is  => 'rw',
		     isa => 'Str',
    );
has 'stp' => (is  => 'rw',
	      isa => 'OrderSelfTradePreventionFlag',
    );

## Limit orders
#
has 'price' => (is  => 'rw',
		isa => 'PositiveNum',
    );
has 'size' => (is  => 'rw',
	       isa => 'PositiveNum',
    );
has 'time_in_force' => (is  => 'rw',
			isa => 'OrderTimeInForce',
    );
has 'cancel_after' => (is  => 'rw',
		       isa => 'Str',
    );
has 'post_only' => (is  => 'rw',
		    isa => 'Bool',
    );

## Market orders
#
has 'funds' => (is  => 'rw',
		isa => 'PositiveNum',
    );

## Stop orders
#
has 'overdraft_enabled' => (is  => 'rw',
			    isa => 'Bool',
    );
has 'funding_amount' => (is  => 'rw',
			 isa => 'PositiveNum',
    );

sub initiate {
    my $self = shift;
    die 'Orders need side, "buy" or "sell"' unless $self->side;
    die 'Orders need a product_id' unless $self->product_id;
    if ($self->type eq 'limit') {
	die 'Limit orders need price' unless $self->price;
	die 'Limit orders need size' unless $self->size;
	if ($self->cancel_after) {
	    unless ($self->time_in_force eq 'GTT') {
		die 'time_in_force must be "GTT" when using cancel_after';
	    }
	}
	if ($self->post_only) {
	    if ($self->time_in_force =~ /^(IOC|FOK)$/) {
		die 'post_only is invalid with time_in_force IOC or FOK';
	    }
	}
    }
    if ($self->type =~ /^(market|stop)$/) {
	unless ($self->size or $self->funds) {
	    die 'market and stop orders must specify either size or funds';
	}
    }
    my %body;
    map { $body{$_} = $self->$_ if defined $self->$_ } @body_attributes;
    $self->body(\%body);
    $self->method('POST');
    $self->path('/orders');
    return $self->send;
}

sub cancel {
    my ($self, $order_id) = @_;
    die 'Order ID is required' unless $order_id;
    $self->method('DELETE');
    $self->path("/orders/$order_id");
    return $self->send;
}

sub cancel_all {
    my ($self, $product_id) = @_;
    my $path = '/orders';
    $path .= "?product_id=$product_id" if $product_id;
    $self->method('DELETE');
    $self->path($path);
    return $self->send;
}

sub list {
    my ($self, $status, $product_id) = @_;
    my $path = '/orders';
    my @qparams;
    if ($status) {
	if (ref($status) ne 'ARRAY') {
	    $status = [$status];
	}
	map { push @qparams, "status=$_" } @$status;
    }
    push @qparams, "product_id=$product_id" if $product_id;
    if (scalar @qparams) {
	$path .= '?' . join('&', @qparams);
    }
    $self->method('GET');
    $self->path($path);
    return $self->send;
}

sub get {
    my ($self, $order_id) = @_;
    die 'Order ID is required' unless $order_id;
    $self->method('GET');
    $self->path("/orders/$order_id");
    return $self->send;
}

__PACKAGE__->meta->make_immutable;
1;

=head1 NAME

Finance::GDAX::API::Order - Perl interface to the GDAX Order API

=head1 SYNOPSIS

  use Finance::GDAX::API::Order;

  $order = Finance::GDAX::API::Order->new;
  $order->side('buy');
  $order->type('market');
  $order->product_id('BTC-USD');
  $order->funds(500.00);
  $response = $order->initiate;
  if ($order->error) { die "Error: ".$order->error };
  

=head1 DESCRIPTION

Basic interface to the GDAX API for orders. PLEASE USE A NEW ORDER
OBJECT FOR EACH ORDER, otherwise unexpected things can happen.

The API shows the response should look something like this for orders
intiated:

  {
    "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
    "price": "0.10000000",
    "size": "0.01000000",
    "product_id": "BTC-USD",
    "side": "buy",
    "stp": "dc",
    "type": "limit",
    "time_in_force": "GTC",
    "post_only": false,
    "created_at": "2016-12-08T20:02:28.53864Z",
    "fill_fees": "0.0000000000000000",
    "filled_size": "0.00000000",
    "executed_value": "0.0000000000000000",
    "status": "pending",
    "settled": false
  }

=head1 ATTRIBUTES (common to all order types)

=head2 C<client_oid>

[optional] Order ID selected by you to identify your order

The optional client_oid field must be a UUID generated by your trading
application. This field value will be broadcast in the public feed for
received messages. You can use this field to identify your orders in
the public feed.

The client_oid is different than the server-assigned order id. If you
are consuming the public feed and see a received message with your
client_oid, you should record the server-assigned order_id as it will
be used for future order status updates. The client_oid will NOT be
used after the received message is sent.

The server-assigned order id is also returned as the id field to this
HTTP POST request.

=head2 C<type>

[optional] limit, market, or stop (default is limit)

limit orders are both the default and basic order type. A limit order
requires specifying a price and size. The size is the number of
bitcoin to buy or sell, and the price is the price per bitcoin. The
limit order will be filled at the price specified or better. A sell
order can be filled at the specified price per bitcoin or a higher
price per bitcoin and a buy order can be filled at the specified price
or a lower price depending on market conditions. If market conditions
cannot fill the limit order immediately, then the limit order will
become part of the open order book until filled by another incoming
order or canceled by the user.

market orders differ from limit orders in that they provide no pricing
guarantees. They however do provide a way to buy or sell specific
amounts of bitcoin or fiat without having to specify the price. Market
orders execute immediately and no part of the market order will go on
the open order book. Market orders are always considered takers and
incur taker fees. When placing a market order you can specify funds
and/or size. Funds will limit how much of your quote currency account
balance is used and size will limit the bitcoin amount transacted.

stop orders become active and wait to trigger based on the movement of
the last trade price. There are two types of stop orders, sell stop
and buy stop.

=head2 C<side>

buy or sell

side: 'sell': Place a sell stop order, which triggers when the last
trade price changes to a value at or below the price.

side: 'buy': Place a buy stop order, which triggers when the last
trade price changes to a value at or above price.

=head2 C<product_id>

A valid product id

The product_id must match a valid product. The products list is
available via the /products endpoint.

=head2 C<stp>

[optional] Self-trade prevention flag

Self-trading is not allowed on GDAX. Two orders from the same user
will not be allowed to match with one another. To change the
self-trade behavior, specify the stp flag.

  Flag 	Name
  dc 	Decrease and Cancel (default)
  co 	Cancel oldest
  cn 	Cancel newest
  cb 	Cancel both

=head1 ATTRIBUTES (limit orders)

=head2 C<price>

Price per bitcoin

The price must be specified in quote_increment product units. The
quote increment is the smallest unit of price. For the BTC-USD
product, the quote increment is 0.01 or 1 penny. Prices less than 1
penny will not be accepted, and no fractional penny prices will be
accepted. Not required for market orders.

=head2 C<size>

Amount of BTC to buy or sell

The size must be greater than the base_min_size for the product and no
larger than the base_max_size. The size can be in any increment of the
base currency (BTC for the BTC-USD product), which includes satoshi
units. size indicates the amount of BTC (or base currency) to buy or
sell.

=head2 C<time_in_force>

[optional] GTC, GTT, IOC, or FOK (default is GTC)

=head2 C<cancel_after>

[optional] min, hour, day

Requires time_in_force to be GTT

Time in force policies provide guarantees about the lifetime of an
order. There are four policies: good till canceled GTC, good till time
GTT, immediate or cancel IOC, and fill or kill FOK.

GTC Good till canceled orders remain open on the book until
canceled. This is the default behavior if no policy is specified.

GTT Good till time orders remain open on the book until canceled or
the allotted cancel_after is depleted on the matching engine. GTT
orders are guaranteed to cancel before any other order is processed
after the cancel_after timestamp which is returned by the API. A day
is considered 24 hours.

IOC Immediate or cancel orders instantly cancel the remaining size of
the limit order instead of opening it on the book.

FOK Fill or kill orders are rejected if the entire size cannot be
matched.

* Note, match also refers to self trades.

=head2 C<post_only>

[optional] Post only flag

Invalid when time_in_force is IOC or FOK

The post-only flag indicates that the order should only make
liquidity. If any part of the order results in taking liquidity, the
order will be rejected and no part of it will execute.

=head1 ATTRIBUTES (market orders)

=head2 C<size>

[optional]* Desired amount in BTC

The size must be greater than the base_min_size for the product and no
larger than the base_max_size. The size can be in any increment of the
base currency (BTC for the BTC-USD product), which includes satoshi
units. size indicates the amount of BTC (or base currency) to buy or
sell.

* One of size or funds is required.

=head2 C<funds>

[optional]* Desired amount of quote currency to use

The funds field is optionally used for market orders. When specified
it indicates how much of the product quote currency to buy or
sell. For example, a market buy for BTC-USD with funds specified as
150.00 will spend 150 USD to buy BTC (including any fees). If the
funds field is not specified for a market buy order, size must be
specified and GDAX will use available funds in your account to buy
bitcoin.

A market sell order can also specify the funds. If funds is specified,
it will limit the sell to the amount of funds specified. You can use
funds with sell orders to limit the amount of quote currency funds
received.

* One of size or funds is required.

=head1 ATTRIBUTES (stop order)

=head2 C<price>

Desired price at which the stop order triggers

=head2 C<size>

[optional]* Desired amount in BTC

=head2 C<funds>

[optional]* Desired amount of quote currency to use

* One of size or funds is required.

=head1 ATTRIBUTES (margin parameters)

=head2 C<overdraft_enabled>

* If true funding will be provided if the order's cost cannot be
  covered by the account's balance

=head2 C<funding_amount>

* Amount of funding to be provided for the order

* Margin can be used to receive funding by specifying either
  overdraft_enabled or funding_amount.

=head1 METHODS

=head2 C<initiate>

Initiates the REST order request and returns the result, according to
the current API docs, like

  {
    "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
    "price": "0.10000000",
    "size": "0.01000000",
    "product_id": "BTC-USD",
    "side": "buy",
    "stp": "dc",
    "type": "limit",
    "time_in_force": "GTC",
    "post_only": false,
    "created_at": "2016-12-08T20:02:28.53864Z",
    "fill_fees": "0.0000000000000000",
    "filled_size": "0.00000000",
    "executed_value": "0.0000000000000000",
    "status": "pending",
    "settled": false
  }

If $order->error exists, then it will contain the error
message. PLEASE USE A NEW ORDER OBJECT FOR EACH ORDER, otherwise
unexpected things can happen.

=head2 C<cancel> $order_id

Cancels the $order_id, where $order_id is the server-assigned order_id
and not the client-selected client_oid.

Probelms will be reported in ->error

=head2 C<cancel_all> ($product_id)

Cancels all orders, or if $product_id is specified, cancels only
orders within those prodcuts.

An array will be returned, according to current API docs:

  [
    "144c6f8e-713f-4682-8435-5280fbe8b2b4",
    "debe4907-95dc-442f-af3b-cec12f42ebda",
    "cf7aceee-7b08-4227-a76c-3858144323ab",
    "dfc5ae27-cadb-4c0c-beef-8994936fde8a",
    "34fecfbf-de33-4273-b2c6-baf8e8948be4"
  ]

=head2 C<list> ($status, $product_id)

Returns a list of orders. You can limit the list by specifying order
status and product_id. These are positional parameters, so status must
come first even if undefined (or 0).

  $list = $order->list;

$status can be an arrayref, in which case it will list each of the
given status.

  $list = $order->list(['open','settled'], 'BTC-USD');

To quote from the API:

=over

Orders which are no longer resting on the order book, will be marked
with the done status. There is a small window between an order being
done and settled. An order is settled when all of the fills have
settled and the remaining holds (if any) have been removed.

=back

By default open, pending and active orders are returned.

=head2 C<get> $order_id

Returns a hash of the specified $order_id

=cut


=head1 AUTHOR

Mark Rushing <mark@orbislumen.net>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Home Grown Systems, SPC.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut