class Model(object, metaclass=Metaclass):
    # Make attribute assignment impossible
    __slots__ = ('_fields', '_str')

    _delimiter = '_'

    def __setattr__(self, key, value):
        raise NotImplementedError

    def __delattr__(self, item):
        raise NotImplementedError

    def __str__(self):
        return self._str

    def __repr__(self):
        def information():

            # These attributes seem to be the most important to users
            for attribute in ('id', 'instrument', 'amount', 'units',
                              'current_units', 'realized_pl',
                              'unrealized_pl', 'price', 'reason', 'time'):
                try:
                    value = getattr(self, attribute)
                except (IndexError, AttributeError):
                    continue
                if value is not None:
                    yield f'{attribute}={value}'

        # Attempt to get important attributes otherwise provide everything
        attributes = ', '.join(information())
        if not attributes:
            attributes = ', '.join(self._fields)

        return f'<{self.__class__.__name__}: {attributes}>'

    def __getattr__(self, item):
        result = self.__getattribute__('_' + item)()
        object.__setattr__(self, item, result)
        return result

    def __hash__(self):
        return hash(self._str)

    def __eq__(self, other):
        try:
            if self._str == other._str:
                return True
        except AttributeError:
            pass
        return False

    def __init__(self, **kwargs):

        # contains all the attributes the class instance contains
        fields = []
        for name, attr in self._preset_values.items():
            fields.append(name)
            object.__setattr__(self, name, attr)

        for name, value in kwargs.items():
            annotation = self.__annotations__[name]
            if value is sentinel:
                pass
            elif value is None:
                fields.append(name)
                object.__setattr__(self, name, None)
            else:
                fields.append(name)
                self._instantiate(name, annotation, value)
        object.__setattr__(self, '_str', f'{self.__class__.__name__}(**{kwargs})')
        object.__setattr__(self, '_fields', tuple(fields))

    def get(self, name, default=None):
        return getattr(self, name, default)

    def replace(self, **kwargs):
        return self.__class__(**dict(self.dict(), **kwargs))

    def dict(self, json=False, datetime_format=None):
        """Convert object into a dictionary representation

        Args:
            json: - bool. True converts dict keys into JSON format
            datetime_format: - str. convert pd.Timestamps to desired format
        """

        def fields():

            for field in self._fields:

                attr = getattr(self, field)

                if not isinstance(attr, (int, float, str, pd.Timestamp)):
                    # Means attr is either a Model object, tuple, list, None
                    try:
                        attr = attr.dict(json=json, datetime_format=datetime_format)
                    except AttributeError:
                        try:
                            attr = [obj.dict(json=json, datetime_format=datetime_format) for obj in attr]
                        except AttributeError:
                            attr = [str(obj)
                                    if json and isinstance(obj, float)
                                    else obj
                                    for obj in attr]
                        except TypeError:
                            # Attr is None. account_changes endpoint
                            # returns items with null
                            attr = attr
                elif json and isinstance(attr, (float, Specifier)):
                    # Technically OANDA's spec declares all specifiers as strings
                    # though TradeID and OrderID in async_v20 are integers. As this
                    # seems to be most useful type. We will make sure to cast them back
                    # to strings when sending JSON data to OANDA
                    attr = str(attr)
                elif isinstance(attr, pd.Timestamp):
                    if json or datetime_format == 'RFC3339':
                        attr = attr.json(datetime_format=datetime_format)
                    elif datetime_format == 'UNIX':
                        attr = attr.value

                yield field, attr

        return {json_attributes[field] if json else field: attr for field, attr in fields()}

    def json(self, datetime_format='UNIX'):
        """Return the JSON representation of the object

        Args:
            datetime_format: either `UNIX` or `RFC3339` controls the representation
              of :class:`~async_v20.definitions.primitives.DataTime` objects"""
        return json.dumps(self.dict(json=True, datetime_format=datetime_format))

    def data(self, json=False, datetime_format=None, delimiter=None):
        """Return the a flattened dictionary representation of the object

        Args:
            json: True, dict will contain OANDA's JSON representation of objects (camelCase),
              False, dict will contain async_v20 representation of objects (snake_case).
            datetime_format: either `UNIX` or `RFC3339` will serialize the date times into the
              corresponding format
            delimiter: Value to use when flattening the the data structure. Defaults to `_`
        """
        if delimiter is None:
            delimiter = self._delimiter
        return flatten_dict(self.dict(json=json, datetime_format=datetime_format), delimiter)

    def series(self, json=False, datetime_format=None, delimiter=None):
        """Return a :class:`pandas.Series` representation of the object

        Args:
            json: True, dict will contain OANDA's JSON representation of objects (camelCase),
              False, dict will contain async_v20 representation of objects (snake_case).
            datetime_format: either `UNIX` or `RFC3339` will serialize the date times into the
              corresponding format
            delimiter: Value to use when flattening the the data structure. Defaults to `_`
        """
        return pd.Series(self.data(json=json, datetime_format=datetime_format, delimiter=delimiter)) class Array(object):
    """Mixin to denote objects that are sent from OANDA in an array.
    Also used to correctly serialize objects.
    """

    def __contains__(self, item):
        """Return True if item in this array or item matches an
        objects id or instrument attribute, False otherwise.

        Note: this traverses all or part of the array, instantiating the
        objects. Using `x in array` may, therefore, have a serious impact on
        performance.

        """
        if str(item) in self._id_index or item in self._instrument_index:
            return True
        for value in self:
            if value == item:
                return True

    def __init__(self, *items):
        """Initialize a new array.

        The *items passed in are assumed to be JSON data. If an item is
        accessed, it is passed to `create_attribute` with the appropriate
        class type.

        Initially, objects are stored in self._items. When accessed, the
        objects are reified and stored in self.items. This is transparently
        handled by self.__getitem__(self, key).

        """
        object.__setattr__(self, '_items', items)
        object.__setattr__(self, 'items', [])

    def __init_subclass__(cls, **kwargs):
        """Record the type *contained in* the subclass-array.

        A subclass like:

            class array_holding_foo(Array, contains=Foo):
                pass

        will have all its inner objects instantiated using class Foo.

        """
        cls._contains = kwargs.pop('contains')

    def __repr__(self):
        return f'<{self._contains.__name__} x {len(self)}>'

    def __hash__(self):
        return hash(str(self._items))

    def __eq__(self, other):
        try:
            if str(self._items) == str(other._items):
                return True
        except AttributeError:
            pass
        return False

    def __len__(self):
        return len(self._items)

    def __iter__(self):
        """Iterate over items in array. Use integer indexing so that
        __getitem__ can handle reifying all the objects.

        """
        for index in range(len(self)):
            yield self[index]

    def __getitem__(self, key):
        if isinstance(key, slice):
            return self.__class__(*[self._items[index]
                                    for index in range(len(self._items))[key]])

        length = len(self._items)
        if key < 0:
            key += length

        if not (0 <= key < length):
            raise IndexError('Array index out of range')

        if key >= len(self.items):
            self.items.__iadd__([None] * (key - len(self.items) + 1))

        if self.items[key] is None:
            json = self._items[key]
            self.items[key] = create_attribute(self._contains, json)

        return self.items[key]

    def __getattr__(self, item):
        try:
            indexes = self._construct_indexes()
            for key, value in indexes.items():
                object.__setattr__(self, key, value)
            return indexes[item]
        except KeyError as e:
            raise AttributeError(e)

    def __add__(self, other):
        return self.__class__(*self, *other)

    __radd__ = __add__

    def __delattr__(self, item):
        raise NotImplementedError

    def __setattr__(self, key, value):
        raise NotImplementedError

    def _construct_indexes(self):
        id_index = {}
        trade_id_index = {}
        instrument_index = {}
        for index, json_model in enumerate(self._items):
            # json_model is either a json_dict. Or a Model object
            # constructed from the json_dict
            try:
                key = json_model.get('id', None)
                if key is not None:
                    id_index.update({str(key): index})


                key = json_model.get('trade_id', json_model.get('tradeID'))
                if key is not None:
                    trade_id_index.setdefault(str(key), []).append(index)

                key = json_model.get('instrument', json_model.get('name', None))
                if key is not None:
                    instrument_index.setdefault(key, []).append(index)
            except AttributeError:
                break

        return dict(
            _id_index=id_index,
            _trade_id_index=trade_id_index,
            _instrument_index=instrument_index
        )

    def get_id(self, id_, default=None):
        """Return the objects in the array where the
        `object.id` attribute matches the passed id
        else return the default"""
        try:
            return self[self._id_index[str(id_)]]
        except KeyError:
            pass
        return default

    def get_trade_id(self, id_, default=None, *, type=None):
        """Return the first object in the array where the
        `object.trade_id` attribute matches the passed id
        else return the default

        Args:
            id_: The trade_id to get.
            default: The default value to return if the id_ cannot be found
            type: The returned objects OANDA type. eg. 'STOP_LOSS'
                not to be confused the `type(instance)`
        """
        try:
            if type is not None:
                for obj in (self[idx] for idx in self._trade_id_index[str(id_)]):
                    if getattr(obj, 'type', None) == type:
                        return obj
                return default

            return self[self._trade_id_index[str(id_)][0]]
        except KeyError:
            pass
        return default

    def get_trade_ids(self, id_, default=None):
        """Return the objects in the array where the
        `object.trade_id` attribute matches the passed id
        else return the default"""
        try:
            return self.__class__(*[self[idx] for idx in self._trade_id_index[str(id_)]])
        except KeyError:
            pass
        return default


    def get_instrument(self, instrument, default=None):
        """Return the first object in the array where
        where the `object.instrument` matches the passed instrument
        else return default"""
        try:
            return self[self._instrument_index[instrument][0]]
        except KeyError:
            pass
        return default

    def get_instruments(self, instrument, default=None):
        """Return all the objects in the array where the
        `object.instrument` attribute matches the passed instrument
        else return the default"""
        try:
            return self.__class__(*[self[idx] for idx in self._instrument_index[instrument]])
        except KeyError:
            pass
        return default

    def dataframe(self, json=False, datetime_format=None):
        """Create a pandas.Dataframe

        Args:
            json: True, DataFrame columns will have the JSON representation,
                False, DataFrame columns will have the object attribute representation

            datetime_format: 'UNIX' or 'RFC3339'
        """
        return pd.DataFrame(obj.data(json=json, datetime_format=datetime_format) for obj in self) def create_attribute(typ, data):
    """Correctly instantiate object based upon type of argument passed"""
    try:
        if isinstance(data, dict):
            result = typ(**data)
        elif isinstance(data, Specifier):
            if not issubclass(typ, Specifier):
                msg = f'{data} must be a {Specifier} is {type(data)}'
                logger.error(msg)
                raise IncompatibleValue(msg)
            result = typ(data)
        elif isinstance(data, InstrumentName):
            result = typ(data)
        elif isinstance(data, (Model, Array, Primitive)):
            if not issubclass(type(data), typ):
                msg = f'{data} must be of type {typ} is {type(data)}'
                logger.error(msg)
                raise IncompatibleValue(msg)
            result = data
        elif isinstance(data, (tuple, list)):
            result = typ(*data)
        else:
            result = typ(data)
    except (TypeError, ValueError, UnknownKeywordArgument):
        # This error handling is required when there is no
        # schema available to parse the data. Typically
        # when an error code has been returned
        # A none value should be returned if this is the case
        if typ is not None:
            msg = f'Could not create {typ}. DATA: {data}, TYPE: {type(data)}'
            logger.error(msg)
            raise InstantiationFailure(msg)
    else:
        return result def flatten_dict(dictionary, delimiter='_'):
    """Flatten a nested dictionary structure"""

    def unpack(parent_key, parent_value):
        """Unpack one level of nesting in a dictionary"""
        try:
            items = parent_value.items()
        except AttributeError:
            # parent_value was not a dict, no need to flatten
            yield (parent_key, parent_value)
        else:
            for key, value in items:
                yield (parent_key + delimiter + key, value)

    while True:
        # Keep unpacking the dictionary until all value's are not dictionary's
        dictionary = dict(chain.from_iterable(starmap(unpack, dictionary.items())))
        if not any(isinstance(value, dict) for value in dictionary.values()):
            break

    return dictionary class TradeID(int, Primitive, Specifier):
    """The Trade’s identifier, unique within the Trade’s Account.
    """

    # Correct syntax of value
    format_syntax = 'The string representation of the OANDA-assigned TradeID. ' \
                    'OANDA-assigned https://github.com/jamespeterschinner/async_v20/issues/12s are positive integers, and are ' \
                    'derived from the TransactionID of the Transaction that opened the Trade.'
    # Example of correct format
    example = '1523'

    def __new__(cls, value):
        return super().__new__(cls, value) class AccountID(str, Primitive):
    """The string representation of an Account Identifier.
    """

    # Correct syntax of value
    format_syntax = '"-"-delimited string with format "{siteID}-{divisionID}-{userID}-{accountNumber}"'
    # Example of correct format
    example = '001-011-5838423-001'

    def __new__(cls, value):
        assert domain_check(value, example=cls.example)
        return super().__new__(cls, value) class Account(AccountSummary):
    """The full details of a client's Account. This includes full open Trade, open
    Position and pending Order representation.

    Attributes:
        id: :class:`~async_v20.AccountID`
            The Account's identifier
        alias: :class:`str`
            Client-assigned alias for the Account. Only provided
            if the Account has an alias set
        currency: :class:`~async_v20.Currency`
            The home currency of the Account
        balance: :class:`~async_v20.AccountUnits`
            The current balance of the Account. Represented in the Account's home currency.
        created_by_user_id: :class:`int`
            ID of the user that created the Account.
        created_time: :class:`~async_v20.DateTime`
            The date/time when the Account was created.
        pl: :class:`~async_v20.AccountUnits`
            The total profit/loss realized over the lifetime of
            the Account. Represented in the Account's home currency.
        resettable_pl: :class:`~async_v20.AccountUnits`
            The total realized profit/loss for the Account since it was
            last reset by the client. Represented in the Account's home currency.
        resettabled_pl_time: :class:`~async_v20.DateTime`
            The date/time that the Account's resettablePL was last reset.
        commission: :class:`~async_v20.AccountUnits`
            The total amount of commission paid over the lifetime
            of the Account. Represented in the Account's home currency.
        margin_rate: :class:`~async_v20.DecimalNumber`
            Client-provided margin rate override for the Account. The effective margin rate of the Account
            is the lesser of this value and
            the OANDA margin rate for the Account's division. This value is only provided if a margin rate override
            exists for the Account.
        margin_call_enter_time: :class:`~async_v20.DateTime`
            The date/time when the Account entered a margin call state.
            Only provided if the Account is in a margin call.
        margin_call_extension_count: :class:`int`
            The number of times that the Account's current margin call was extended.
        last_margin_call_extension_time: :class:`~async_v20.DateTime`
            The date/time of the Account's last margin call extension.
        open_trade_count: :class:`int`
            The number of Trades currently open in the Account.
        open_position_count: :class:`int`
            The number of Positions currently open in the Account.
        pending_order_count: :class:`int`
            The number of Orders currently pending in the Account.
        hedging_enabled: :class:`bool`
            Flag indicating that the Account has hedging enabled.
        unrealized_pl: :class:`~async_v20.AccountUnits`
            The total unrealized profit/loss for all Trades currently open
            in the Account. Represented in the Account's home currency.
        nav: :class:`~async_v20.AccountUnits`
            The net asset value of the Account. Equal to
            Account balance + unrealizedPL. Represented in the Account's home currency.
        margin_used: :class:`~async_v20.AccountUnits`
            Margin currently used for the Account.
            Represented in the Account's home currency.
        margin_available: :class:`~async_v20.AccountUnits`
            Margin available for Account. Represented in the Account's home currency.
        position_value: :class:`~async_v20.AccountUnits`
            The value of the Account's open
            positions represented in the Account's home currency.
        margin_closeout_unrealized_pl: :class:`~async_v20.AccountUnits`
            The Account's margin closeout unrealized PL.
        margin_closeout_nav: :class:`~async_v20.AccountUnits`
            The Account's margin closeout NAV.
        margin_closeout_margin_used: :class:`~async_v20.AccountUnits`
            The Account's margin closeout margin used.
        margin_closeout_percent: :class:`~async_v20.DecimalNumber`
            The Account's margin closeout percentage. When this value is 1.0
            or above the Account is in a margin closeout situation.
        margin_closeout_position_value: :class:`~async_v20.DecimalNumber`
            The value of the Account's open positions as used
            for margin closeout calculations represented in the Account's home currency.
        withdrawal_limit: :class:`~async_v20.AccountUnits`
            The current WithdrawalLimit for the account which will be zero or
            a positive value indicating how much can be withdrawn from the account.
        margin_call_margin_used: :class:`~async_v20.AccountUnits`
            The Account's margin call margin used.
        margin_call_percent: :class:`~async_v20.DecimalNumber`
            The Account's margin call percentage. When this value is 1.0
            or above the Account is in a margin call situation.
        last_transaction_id: :class:`~async_v20.TransactionID`
            The ID of the last Transaction created for the Account.
        trades: ( :class:`~async_v20.TradeSummary`, ...)
            The details of the Trades currently open in the Account.
        positions: ( :class:`~async_v20.Position`, ...)
            The details all Account Positions.
        orders: ( :class:`~async_v20.Order`, ...)
            The details of the Orders currently pending in the Account.
        dividend: :class:`~async_v20.DecimalNumber`
            Dividend
        dividendAdjustment: :class:`~async_v20.DecimalNumber`
            Undocumented
    """

    def __init__(self, id: AccountID = sentinel, alias: str = sentinel, currency: Currency = sentinel,
                 balance: AccountUnits = sentinel,
                 created_by_user_id: int = sentinel, created_time: DateTime = sentinel, pl: AccountUnits = sentinel,
                 resettable_pl: AccountUnits = sentinel, resettabled_pl_time: DateTime = sentinel,
                 commission: AccountUnits = sentinel, margin_rate: DecimalNumber = sentinel,
                 margin_call_enter_time: DateTime = sentinel, margin_call_extension_count: int = sentinel,
                 last_margin_call_extension_time: DateTime = sentinel, open_trade_count: int = sentinel,
                 open_position_count: int = sentinel, pending_order_count: int = sentinel,
                 hedging_enabled: bool = sentinel,
                 unrealized_pl: AccountUnits = sentinel, nav: AccountUnits = sentinel,
                 margin_used: AccountUnits = sentinel,
                 margin_available: AccountUnits = sentinel, position_value: AccountUnits = sentinel,
                 margin_closeout_unrealized_pl: AccountUnits = sentinel, margin_closeout_nav: AccountUnits = sentinel,
                 margin_closeout_margin_used: AccountUnits = sentinel,
                 margin_closeout_percent: DecimalNumber = sentinel,
                 margin_closeout_position_value: DecimalNumber = sentinel, withdrawal_limit: AccountUnits = sentinel,
                 margin_call_margin_used: AccountUnits = sentinel, margin_call_percent: DecimalNumber = sentinel,
                 last_transaction_id: TransactionID = sentinel, trades: ArrayTradeSummary = sentinel,
                 positions: ArrayPosition = sentinel, orders: ArrayOrder = sentinel,
                 financing: DecimalNumber = sentinel,
                 guaranteed_stop_loss_order_mode: GuaranteedStopLossOrderMode = sentinel,
                 resettable_pl_time: DateTime = sentinel,
                 dividend: DecimalNumber = sentinel,
                 dividend_adjustment: AccountUnits = sentinel,
                 guaranteed_execution_fees: AccountUnits = sentinel):
        Model.__init__(**locals()) class ArrayInstrument(Array, contains=Instrument, one_to_many=False):
    pass class ArrayOrder(Array, contains=Order):
    pass class ArrayPosition(Array, contains=Position, one_to_many=False):
    pass class ArrayStr(Array, contains=str):
    pass class ArrayTrade(Array, contains=Trade):
    pass class ArrayTransaction(Array, contains=Transaction):
    pass class Order(Model):
    """The base Order definition. Contains all possible attributes an Order
    may contain

    Attributes:
        id: :class:`~async_v20.OrderID`
            The Order's identifier, unique within the Order's Account.
        create_time: :class:`~async_v20.DateTime`
            The time when the Order was created.
        state: :class:`~async_v20.OrderState`
            The current state of the Order.
        client_extensions: :class:`~async_v20.ClientExtensions`
            The client extensions of the Order. Do not set, modify,
            or delete clientExtensions if your account is associated with MT4.
        trade_id: :class:`~async_v20.TradeID`
        price: :class:`~async_v20.PriceValue`
        type: :class:`~async_v20.OrderType`
        client_trade_id: :class:`~async_v20.ClientID`
        time_in_force: :class:`~async_v20.TimeInForce`
        gtd_time: :class:`~async_v20.DateTime`
        trigger_condition: :class:`~async_v20.OrderTriggerCondition`
        filling_transaction_id: :class:`~async_v20.TransactionID`
        filled_time: :class:`~async_v20.DateTime`
        trade_opened_id: :class:`~async_v20.TradeID`
        trade_reduced_id: :class:`~async_v20.TradeID`
        trade_closed_ids: ( :class:`~async_v20.TradeID`, ...),
        cancelling_transaction_id: :class:`~async_v20.TransactionID`
        cancelled_time: :class:`~async_v20.DateTime`
        replaces_order_id: :class:`~async_v20.OrderID`
        replaced_by_order_id: :class:`~async_v20.OrderID`
        distance: :class:`~async_v20.PriceValue`
        trailing_stop_value: :class:`~async_v20.PriceValue`
        instrument: :class:`~async_v20.InstrumentName`
        units: :class:`~async_v20.DecimalNumber`
        partial_fill: :class:`str`
        position_fill: :class:`~async_v20.OrderPositionFill`
        take_profit_on_fill: :class:`~async_v20.TakeProfitDetails`
        stop_loss_on_fill: :class:`~async_v20.StopLossDetails`
        trailing_stop_loss_on_fill: :class:`~async_v20.TrailingStopLossDetails`
        trade_client_extensions: :class:`~async_v20.ClientExtensions`
        price_bound: :class:`~async_v20.PriceValue`
        initial_market_price: :class:`~async_v20.PriceValue`
        trade_close: :class:`~async_v20.MarketOrderTradeClose`
        long_position_closeout: :class:`~async_v20.MarketOrderPositionCloseout`
        short_position_closeout: :class:`~async_v20.MarketOrderPositionCloseout`
        margin_closeout: :class:`~async_v20.MarketOrderMarginCloseout`
        delayed_trade_close: :class:`~async_v20.MarketOrderDelayedTradeClose`
        trigger_distance: :class:`~async_v20.PriceValue`
        is_trigger_distance_exact: :class:`bool`
    """

    # TODO: Update the annotation for partial_fill when OANDA responds to email, & `guaranteed`

    def __init__(self, id: OrderID = sentinel, create_time: DateTime = sentinel, state: OrderState = sentinel,
                 client_extensions: ClientExtensions = sentinel, trade_id: TradeID = sentinel,
                 price: PriceValue = sentinel,
                 type: OrderType = sentinel, client_trade_id: ClientID = sentinel,
                 time_in_force: TimeInForce = sentinel,
                 gtd_time: DateTime = sentinel, trigger_condition: OrderTriggerCondition = sentinel,
                 filling_transaction_id: TransactionID = sentinel, filled_time: DateTime = sentinel,
                 trade_opened_id: TradeID = sentinel, trade_reduced_id: TradeID = sentinel,
                 trade_closed_ids: ArrayTradeID = sentinel, cancelling_transaction_id: TransactionID = sentinel,
                 cancelled_time: DateTime = sentinel, replaces_order_id: OrderID = sentinel,
                 replaced_by_order_id: OrderID = sentinel, distance: PriceValue = sentinel,
                 trailing_stop_value: PriceValue = sentinel, instrument: InstrumentName = sentinel,
                 units: DecimalNumber = sentinel,
                 partial_fill: str = sentinel, position_fill: OrderPositionFill = sentinel,
                 take_profit_on_fill: TakeProfitDetails = sentinel,
                 stop_loss_on_fill: StopLossDetails = sentinel,
                 trailing_stop_loss_on_fill: TrailingStopLossDetails = sentinel,
                 trade_client_extensions: ClientExtensions = sentinel, price_bound: PriceValue = sentinel,
                 initial_market_price: PriceValue = sentinel, trade_close: MarketOrderTradeClose = sentinel,
                 long_position_closeout: MarketOrderPositionCloseout = sentinel,
                 short_position_closeout: MarketOrderPositionCloseout = sentinel,
                 margin_closeout: MarketOrderMarginCloseout = sentinel,
                 delayed_trade_close: MarketOrderDelayedTradeClose = sentinel,
                 trigger_distance: PriceValue = sentinel, is_trigger_distance_exact: bool = sentinel,
                 guaranteed: bool = sentinel
                 ):
        Model.__init__(**locals()) class Position(Model):
    """The specification of a Position within an Account.

    Attributes:
        instrument: :class:`~async_v20.InstrumentName`
            The Position's Instrument.
        pl: :class:`~async_v20.AccountUnits`
            Profit/loss realized by the Position over the lifetime of the Account.
        unrealized_pl: :class:`~async_v20.AccountUnits`
            The unrealized profit/loss of all open Trades that contribute to this Position.
        resettable_pl: :class:`~async_v20.AccountUnits`
            Profit/loss realized by the Position since the
            Account's resettablePL was last reset by the client.
        commission: :class:`~async_v20.AccountUnits`
            The total amount of commission paid for this instrument over
            the lifetime of the Account. Represented in the Account's home currency.
        long: :class:`~async_v20.PositionSide`
            The details of the long side of the Position.
        short: :class:`~async_v20.PositionSide`
            The details of the short side of the Position.

    """

    def __init__(self, instrument: InstrumentName = sentinel, pl: AccountUnits = sentinel,
                 unrealized_pl: AccountUnits = sentinel,
                 resettable_pl: AccountUnits = sentinel, commission: AccountUnits = sentinel,
                 long: PositionSide = sentinel,
                 short: PositionSide = sentinel, financing: DecimalNumber = sentinel,
                 margin_used: AccountUnits = sentinel,
                 guaranteed_execution_fees: AccountUnits = sentinel):
        Model.__init__(**locals()) class Trade(Model):
    """The specification of a Trade within an Account. This includes the full
    representation of the Trade's dependent Orders in addition to the IDs of
    those Orders.

    Attributes:
        id: :class:`~async_v20.TradeID`
            The Trade's identifier, unique within the Trade's Account.
        instrument: :class:`~async_v20.InstrumentName`
            The Trade's Instrument.
        price: :class:`~async_v20.PriceValue`
            The execution price of the Trade.
        open_time: :class:`~async_v20.DateTime`
            The date/time when the Trade was opened.
        state: :class:`~async_v20.TradeState`
            The current state of the Trade.
        initial_units: :class:`~async_v20.DecimalNumber`
            The initial size of the Trade. Negative values indicate
            a short Trade, and positive values indicate a long Trade.
        initial_margin_required: :class:`~async_v20.AccountUnits`
            The margin required at the time the Trade was created. Note, this is the
            ‘pure’ margin required, it is not the ‘effective’ margin used that
            factors in the trade risk if a GSLO is attached to the trade.
        current_units: :class:`~async_v20.DecimalNumber`
            The number of units currently open for the Trade. This
            value is reduced to 0.0 as the Trade is closed.
        realized_pl: :class:`~async_v20.AccountUnits`
            The total profit/loss realized on the closed portion of the Trade.
        unrealized_pl: :class:`~async_v20.AccountUnits`
            The unrealized profit/loss on the open portion of the Trade.
        average_close_price: :class:`~async_v20.PriceValue`
            The average closing price of the Trade. Only present if
            the Trade has been closed or reduced at least once.
        closing_transaction_ids: ( :class:`~async_v20.TransactionID`, ...)
            The IDs of the Transactions that have closed portions of this Trade.
        financing: :class:`~async_v20.AccountUnits`
            The financing paid/collected for this Trade.
        close_time: :class:`~async_v20.DateTime`
            The date/time when the Trade was fully closed.
            Only provided for Trades whose state is CLOSED.
        client_extensions: :class:`~async_v20.ClientExtensions`
            The client extensions of the Trade.
        take_profit_order: :class:`~async_v20.TakeProfitOrder`
            Full representation of the Trade's Take Profit
            Order, only provided if such an Order exists.
        stop_loss_order: :class:`~async_v20.StopLossOrder`
            Full representation of the Trade's Stop Loss
            Order, only provided if such an Order exists.
        trailing_stop_loss_order: :class:`~async_v20.TrailingStopLossOrder`
            Full representation of the Trade's Trailing Stop Loss
            Order, only provided if such an Order exists.
        margin_used:
            Margin currently used by the Trade.

    """

    def __init__(self, id: TradeID = sentinel, instrument: InstrumentName = sentinel, price: PriceValue = sentinel,
                 open_time: DateTime = sentinel, state: TradeState = sentinel, initial_units: DecimalNumber = sentinel,
                 initial_margin_required: AccountUnits = sentinel,
                 current_units: DecimalNumber = sentinel, realized_pl: AccountUnits = sentinel,
                 unrealized_pl: AccountUnits = sentinel, average_close_price: PriceValue = sentinel,
                 closing_transaction_ids: ArrayTransactionID = sentinel, financing: AccountUnits = sentinel,
                 close_time: DateTime = sentinel, client_extensions: ClientExtensions = sentinel,
                 take_profit_order: TakeProfitOrder = sentinel, stop_loss_order: StopLossOrder = sentinel,
                 trailing_stop_loss_order: TrailingStopLossOrder = sentinel,
                 margin_used: AccountUnits = sentinel):
        Model.__init__(**locals()) class TradeSummary(Model):
    """The summary of a Trade within an Account. This representation does not
    provide the full details of the Trade's dependent Orders.

    Attributes:
        id: :class:`~async_v20.TradeID`
            The Trade's identifier, unique within the Trade's Account.
        instrument: :class:`~async_v20.InstrumentName`
            The Trade's Instrument.
        price: :class:`~async_v20.PriceValue`
            The execution price of the Trade.
        open_time: :class:`~async_v20.DateTime`
            The date/time when the Trade was opened.
        state: :class:`~async_v20.TradeState`
            The current state of the Trade.
        initial_units: :class:`~async_v20.DecimalNumber`
            The initial size of the Trade. Negative values indicate
            a short Trade, and positive values indicate a long Trade.
        initial_margin_required: :class:`~async_v20.AccountUnits`
            The margin required at the time the Trade was created. Note, this is the
            ‘pure’ margin required, it is not the ‘effective’ margin used that
            factors in the trade risk if a GSLO is attached to the trade.
        current_units: :class:`~async_v20.DecimalNumber`
            The number of units currently open for the Trade. This
            value is reduced to 0.0 as the Trade is closed.
        realized_pl: :class:`~async_v20.AccountUnits`
            The total profit/loss realized on the closed portion of the Trade.
        unrealized_pl: :class:`~async_v20.AccountUnits`
            The unrealized profit/loss on the open portion of the Trade.
        average_close_price: :class:`~async_v20.PriceValue`
            The average closing price of the Trade. Only present if
            the Trade has been closed or reduced at least once.
        closing_transaction_ids: ( :class:`~async_v20.TransactionID`, ...)
            The IDs of the Transactions that have closed portions of this Trade.
        financing: :class:`~async_v20.AccountUnits`
            The financing paid/collected for this Trade.
        close_time: :class:`~async_v20.DateTime`
            The date/time when the Trade was fully closed.
            Only provided for Trades whose state is CLOSED.
        client_extensions: :class:`~async_v20.ClientExtensions`
            The client extensions of the Trade.
        take_profit_order_id: :class:`~async_v20.OrderID`
            ID of the Trade's Take Profit Order, only provided if such an Order exists.
        stop_loss_order_id: :class:`~async_v20.OrderID`
            ID of the Trade's Stop Loss Order, only provided if such an Order exists.
        trailing_stop_loss_order_id: :class:`~async_v20.OrderID`
            ID of the Trade's Trailing Stop Loss
            Order, only provided if such an Order exists.
        margin_used:
            Margin currently used by the Trade.

    """

    def __init__(self, id: TradeID = sentinel, instrument: InstrumentName = sentinel, price: PriceValue = sentinel,
                 open_time: DateTime = sentinel, state: TradeState = sentinel, initial_units: DecimalNumber = sentinel,
                 initial_margin_required: AccountUnits = sentinel,
                 current_units: DecimalNumber = sentinel, realized_pl: AccountUnits = sentinel,
                 unrealized_pl: AccountUnits = sentinel,
                 average_close_price: PriceValue = sentinel, closing_transaction_ids: ArrayTransactionID = sentinel,
                 financing: AccountUnits = sentinel, close_time: DateTime = sentinel,
                 client_extensions: ClientExtensions = sentinel, take_profit_order_id: OrderID = sentinel,
                 stop_loss_order_id: OrderID = sentinel, trailing_stop_loss_order_id: OrderID = sentinel,
                 margin_used: AccountUnits = sentinel):
        Model.__init__(**locals()) class InstantiationFailure(AsyncV20Exception):
    """async_v20 was unable to create an object from the passed arguments"""
    pass class IncompatibleValue(AsyncV20Exception):
    """A supplied argument is different than the predefined value"""
    pass @pytest.yield_fixture
@pytest.mark.asyncio
async def client():
    oanda_client = OandaClient(rest_host='127.0.0.1', rest_port=8080, rest_scheme='http',
                               stream_host='127.0.0.1', stream_port=8080, stream_scheme='http',
                               health_host='127.0.0.1', health_port=8080, health_scheme='http')
    yield oanda_client
    await oanda_client.close()
    await asyncio.sleep(0) @pytest.yield_fixture
@pytest.mark.asyncio
async def server(event_loop):
    global status, sleep_time
    status = 200
    sleep_time = 0
    server = await event_loop.create_server(web.Server(handler), "127.0.0.1", 8080)
    yield server
    server.close()
    await asyncio.sleep(0)
import logging
import ujson as json
import numpy as np
import pandas as pd
import pytest
from pandas import DataFrame
from async_v20.definitions.base import Model, Array, create_attribute
from async_v20.definitions.helpers import flatten_dict
from async_v20.definitions.primitives import TradeID, AccountID
from async_v20.definitions.types import Account
from async_v20.definitions.types import ArrayInstrument
from async_v20.definitions.types import ArrayOrder
from async_v20.definitions.types import ArrayPosition
from async_v20.definitions.types import ArrayStr
from async_v20.definitions.types import ArrayTrade
from async_v20.definitions.types import ArrayTransaction
from async_v20.definitions.types import Order
from async_v20.definitions.types import Position
from async_v20.definitions.types import Trade
from async_v20.definitions.types import TradeSummary
from async_v20.exceptions import InstantiationFailure, IncompatibleValue
from ..data.json_data import GETAccountID_response, example_trade_summary, example_changed_trade_summary
from ..data.json_data import account_example
from ..data.json_data import example_transactions, example_positions, example_instruments, example_trade_array
from ..fixtures.client import client
from ..fixtures.server import server
from pandas import Timestamp
def account():
    result = Account(**GETAccountID_response['account'])
    yield result
    del result


def test_account_has_correct_methods(account):
    assert hasattr(account, 'dict')
    assert hasattr(account, 'data')
    assert hasattr(account, 'series')


@pytest.fixture
def test_kwargs():
    kwargs = {'type': 'LIST', 'value': 'TEST_VALUE'}
    yield kwargs
    del kwargs


@pytest.fixture
def test_class():
    class TestClass(Model):
        _dispatch = {'type': 'LIST'}

    test_cls = TestClass
    yield test_cls
    del test_cls


def test_base_dispatch_works_correctly():
    pass


def test_json_dict_returns_correct_data_structure(account):
    """Test the result is formatted correctly. There is a requirement for
    json_dict to be able to cast floats to strings, this is necessary when
    serializing objects to send to OANDA. Though when used internally is it more
    natural to leave floats as floats."""

    result = account.dict(json=True, datetime_format='UNIX')
    # Test result is a dict
    assert type(result) == dict
    flattened_result = flatten_dict(result)
    # Test that all values have the correct data type
    for value in flattened_result:
        assert isinstance(value, (dict, str, int, list))

    result = account.dict(json=False, datetime_format='UNIX')
    assert type(result) == dict
    flattened_result = flatten_dict(result)
    # Test that all values have the correct data type. Specifically that
    # all floats have not been casted to a string.
    for value in flattened_result:
        assert isinstance(value, (dict, float, str, int, list))
        if isinstance(value, str):
            with pytest.raises(ValueError):
                float(value)


def test_json_data(account):
    result = account.json(datetime_format='UNIX')
    assert type(result) == str
    assert json.loads(result) == account.dict(json=True, datetime_format='UNIX')


def test_data(account):
    result = account.data(json=True, datetime_format='UNIX')
    for value in result:
        assert isinstance(value, (str, int, list))

    result = account.data(json=False)
    for value in result:
        assert isinstance(value, (float, str, int, list))
        if isinstance(value, str):
            with pytest.raises(ValueError):
                float(value)


def test_series_doesnt_convert_datetime(account):
    result = account.series(datetime_format='UNIX')
    for value in result:
        assert isinstance(value, (float, str, int, list, type(None)))
        if isinstance(value, str):
            # All values in a series object should be a float if they can be
            with pytest.raises(ValueError):
                float(value)


def test_series_converts_time_to_datetime(account):
    result = account.series()
    with pytest.raises(AssertionError):
        for value in result:
            assert isinstance(value, (float, str, int, list, type(None)))

    for value in result:
        assert isinstance(value, (float, str, int, list, type(None), Timestamp))


def test_array_returns_instantiation_error():
    class ArrayTest(Array, contains=int):
        pass

    with pytest.raises(InstantiationFailure):
        instance = ArrayTest('ABC', 'DEF')
        result = instance[0]


def test_array_with_no_dict_does_not_error_when_attempting_get_id():
    class ArrayTest(Array, contains=int):
        pass

    instance = ArrayTest('ABC', 'DEF')
    result = instance.get_id(1)


def test_create_attribute_returns_incompatible_error():
    with pytest.raises(IncompatibleValue):
        create_attribute(AccountID, TradeID(123))

    with pytest.raises(IncompatibleValue):

        create_attribute(ArrayStr, TradeID(123))