diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt
index 5e84f6086d..fc480d2558 100644
--- a/apps/openmw/CMakeLists.txt
+++ b/apps/openmw/CMakeLists.txt
@@ -84,7 +84,7 @@ add_openmw_dir (mwmechanics
     drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor
     aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting
     disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning
-    character actors objects aistate coordinateconverter
+    character actors objects aistate coordinateconverter trading
     )
 
 add_openmw_dir (mwstate
diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp
index ed81677e46..e191c16bb4 100644
--- a/apps/openmw/mwgui/tradewindow.cpp
+++ b/apps/openmw/mwgui/tradewindow.cpp
@@ -4,8 +4,6 @@
 #include <MyGUI_InputManager.h>
 #include <MyGUI_ControllerManager.h>
 
-#include <components/misc/rng.hpp>
-
 #include <components/widgets/numericeditbox.hpp>
 
 #include "../mwbase/environment.hpp"
@@ -19,8 +17,8 @@
 #include "../mwworld/containerstore.hpp"
 #include "../mwworld/esmstore.hpp"
 
-#include "../mwmechanics/creaturestats.hpp"
 #include "../mwmechanics/actorutil.hpp"
+#include "../mwmechanics/creaturestats.hpp"
 
 #include "inventorywindow.hpp"
 #include "itemview.hpp"
@@ -323,77 +321,23 @@ namespace MWGui
             }
         }
 
-        // TODO: move to mwmechanics
+        bool offerAccepted = mTrading.haggle(player, mPtr, mCurrentBalance, mCurrentMerchantOffer);
 
-        // Is the player buying?
-        bool buying = (mCurrentMerchantOffer < 0);
+        // apply disposition change if merchant is NPC
+        if ( mPtr.getClass().isNpc() ) {
+            int dispositionDelta = offerAccepted
+                ? gmst.find("iBarterSuccessDisposition")->getInt()
+                : gmst.find("iBarterFailDisposition")->getInt();
 
-        if(mCurrentBalance > mCurrentMerchantOffer)
-        {
-            //if npc is a creature: reject (no haggle)
-            if (mPtr.getTypeName() != typeid(ESM::NPC).name())
-            {
-                MWBase::Environment::get().getWindowManager()->
-                    messageBox("#{sNotifyMessage9}");
-                return;
-            }
-
-            int a = abs(mCurrentMerchantOffer);
-            int b = abs(mCurrentBalance);
-            int d = 0;
-            if (buying)
-                d = int(100 * (a - b) / a);
-            else
-                d = int(100 * (b - a) / a);
-
-            int clampedDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mPtr);
-
-            const MWMechanics::CreatureStats &sellerStats = mPtr.getClass().getCreatureStats(mPtr);
-            const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player);
-
-            float a1 = static_cast<float>(player.getClass().getSkill(player, ESM::Skill::Mercantile));
-            float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified();
-            float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified();
-            float d1 = static_cast<float>(mPtr.getClass().getSkill(mPtr, ESM::Skill::Mercantile));
-            float e1 = 0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified();
-            float f1 = 0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified();
-
-            float dispositionTerm = gmst.find("fDispositionMod")->getFloat() * (clampedDisposition - 50);
-            float pcTerm = (dispositionTerm - 50 + a1 + b1 + c1) * playerStats.getFatigueTerm();
-            float npcTerm = (d1 + e1 + f1) * sellerStats.getFatigueTerm();
-            float x = gmst.find("fBargainOfferMulti")->getFloat() * d + gmst.find("fBargainOfferBase")->getFloat();
-            if (buying)
-                x += abs(int(pcTerm - npcTerm));
-            else
-                x += abs(int(npcTerm - pcTerm));
-
-            int roll = Misc::Rng::rollDice(100) + 1;
-            if(roll > x || (mCurrentMerchantOffer < 0) != (mCurrentBalance < 0)) //trade refused
-            {
-                MWBase::Environment::get().getWindowManager()->
-                    messageBox("#{sNotifyMessage9}");
-
-                int iBarterFailDisposition = gmst.find("iBarterFailDisposition")->getInt();
-                if (mPtr.getClass().isNpc())
-                    MWBase::Environment::get().getDialogueManager()->applyDispositionChange(iBarterFailDisposition);
-                return;
-            }
-
-            //skill use!
-            float skillGain = 0.f;
-            int finalPrice = std::abs(mCurrentBalance);
-            int initialMerchantOffer = std::abs(mCurrentMerchantOffer);
-            if (!buying && (finalPrice > initialMerchantOffer) && finalPrice > 0)
-                skillGain = floor(100 * (finalPrice - initialMerchantOffer) / float(finalPrice));
-            else if (buying && (finalPrice < initialMerchantOffer) && initialMerchantOffer > 0)
-                skillGain = floor(100 * (initialMerchantOffer - finalPrice) / float(initialMerchantOffer));
-
-            player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain);
+            MWBase::Environment::get().getDialogueManager()->applyDispositionChange(dispositionDelta);
         }
 
-        int iBarterSuccessDisposition = gmst.find("iBarterSuccessDisposition")->getInt();
-        if (mPtr.getClass().isNpc())
-            MWBase::Environment::get().getDialogueManager()->applyDispositionChange(iBarterSuccessDisposition);
+        // display message on haggle failure
+        if ( !offerAccepted ) {
+            MWBase::Environment::get().getWindowManager()->
+                messageBox("#{sNotifyMessage9}");
+            return;
+        }
 
         // make the item transfer
         mTradeModel->transferItems();
diff --git a/apps/openmw/mwgui/tradewindow.hpp b/apps/openmw/mwgui/tradewindow.hpp
index a23196d70e..4b03c8d904 100644
--- a/apps/openmw/mwgui/tradewindow.hpp
+++ b/apps/openmw/mwgui/tradewindow.hpp
@@ -1,6 +1,8 @@
 #ifndef MWGUI_TRADEWINDOW_H
 #define MWGUI_TRADEWINDOW_H
 
+#include "../mwmechanics/trading.hpp"
+
 #include "referenceinterface.hpp"
 #include "windowbase.hpp"
 
@@ -40,6 +42,7 @@ namespace MWGui
             ItemView* mItemView;
             SortFilterItemModel* mSortModel;
             TradeItemModel* mTradeModel;
+            MWMechanics::Trading mTrading;
 
             static const float sBalanceChangeInitialPause; // in seconds
             static const float sBalanceChangeInterval; // in seconds
diff --git a/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp
new file mode 100644
index 0000000000..c4ef5103e3
--- /dev/null
+++ b/apps/openmw/mwmechanics/trading.cpp
@@ -0,0 +1,84 @@
+#include "trading.hpp"
+
+#include <components/misc/rng.hpp>
+
+#include "../mwbase/environment.hpp"
+#include "../mwbase/mechanicsmanager.hpp"
+#include "../mwbase/world.hpp"
+
+#include "../mwmechanics/creaturestats.hpp"
+
+#include "../mwworld/class.hpp"
+#include "../mwworld/esmstore.hpp"
+
+namespace MWMechanics
+{
+    Trading::Trading() {}
+
+    bool Trading::haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer)
+    {
+        // accept if merchant offer is better than player offer
+        if ( playerOffer <= merchantOffer ) {
+            return true;
+        }
+
+        // reject if npc is a creature
+        if ( merchant.getTypeName() != typeid(ESM::NPC).name() ) {
+            return false;
+        }
+
+        const MWWorld::Store<ESM::GameSetting> &gmst =
+            MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
+
+        // Is the player buying?
+        bool buying = (merchantOffer < 0);
+
+        int a = std::abs(merchantOffer);
+        int b = std::abs(playerOffer);
+        int d = (buying)
+            ? int(100 * (a - b) / a)
+            : int(100 * (b - a) / a);
+
+        int clampedDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(merchant);
+
+        const MWMechanics::CreatureStats &merchantStats = merchant.getClass().getCreatureStats(merchant);
+        const MWMechanics::CreatureStats &playerStats = player.getClass().getCreatureStats(player);
+
+        float a1 = static_cast<float>(player.getClass().getSkill(player, ESM::Skill::Mercantile));
+        float b1 = 0.1f * playerStats.getAttribute(ESM::Attribute::Luck).getModified();
+        float c1 = 0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified();
+        float d1 = static_cast<float>(merchant.getClass().getSkill(merchant, ESM::Skill::Mercantile));
+        float e1 = 0.1f * merchantStats.getAttribute(ESM::Attribute::Luck).getModified();
+        float f1 = 0.2f * merchantStats.getAttribute(ESM::Attribute::Personality).getModified();
+
+        float dispositionTerm = gmst.find("fDispositionMod")->getFloat() * (clampedDisposition - 50);
+        float pcTerm = (dispositionTerm - 50 + a1 + b1 + c1) * playerStats.getFatigueTerm();
+        float npcTerm = (d1 + e1 + f1) * merchantStats.getFatigueTerm();
+        float x = gmst.find("fBargainOfferMulti")->getFloat() * d
+            + gmst.find("fBargainOfferBase")->getFloat()
+            + std::abs(int(pcTerm - npcTerm));
+
+        int roll = Misc::Rng::rollDice(100) + 1;
+
+        // reject if roll fails
+        // (or if player tries to buy things and get money)
+        if ( roll > x || (merchantOffer < 0 && 0 < playerOffer) ) {
+            return false;
+        }
+
+        // apply skill gain on successful barter
+        float skillGain = 0.f;
+        int finalPrice = std::abs(playerOffer);
+        int initialMerchantOffer = std::abs(merchantOffer);
+
+        if ( !buying && (finalPrice > initialMerchantOffer) ) {
+            skillGain = floor(100.f * (finalPrice - initialMerchantOffer) / finalPrice);
+        }
+        else if ( buying && (finalPrice < initialMerchantOffer) ) {
+            skillGain = floor(100.f * (initialMerchantOffer - finalPrice) / initialMerchantOffer);
+        }
+        player.getClass().skillUsageSucceeded(player, ESM::Skill::Mercantile, 0, skillGain);
+
+        return true;
+    }
+}
diff --git a/apps/openmw/mwmechanics/trading.hpp b/apps/openmw/mwmechanics/trading.hpp
new file mode 100644
index 0000000000..e006370dde
--- /dev/null
+++ b/apps/openmw/mwmechanics/trading.hpp
@@ -0,0 +1,17 @@
+#ifndef OPENMW_MECHANICS_TRADING_H
+#define OPENMW_MECHANICS_TRADING_H
+
+#include "../mwworld/ptr.hpp"
+
+namespace MWMechanics
+{
+    class Trading
+    {
+    public:
+        Trading();
+
+        bool haggle(const MWWorld::Ptr& player, const MWWorld::Ptr& merchant, int playerOffer, int merchantOffer);
+    };
+}
+
+#endif