/**
*Blitz Trading
*/
package executionserver.fix;
import executionserver.controller.CustomisationController;
import executionserver.controller.DatabaseController;
import executionserver.controller.ExecutionServerController;
import executionserver.domain.Command;
import executionserver.domain.Connection;
import executionserver.domain.ConnectionInfo;
import executionserver.domain.Customisation;
import executionserver.domain.ExecutionOrder;
import java.io.FileNotFoundException;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.bind.JAXBException;
import org.apache.commons.beanutils.BeanUtils;
import org.slf4j.LoggerFactory;
import quickfix.*;
import quickfix.field.ExecTransType;
import quickfix.field.MsgType;
import quickfix.field.RefTagID;
import quickfix.field.Text;
import quickfix.field.TimeInForce;
/**
* Abstract class with default fix connection and processing operations.
*
* @author Sylvio Azevedo
*/
public abstract class AbstractFixConnection implements FixConnection, Application {
// class logger.
private final org.slf4j.Logger logger = LoggerFactory.getLogger(getClass());
// configuration object of fix customisation fields.
protected Customisation customisation = null;
// fix security list ids.
protected Map<String, String> securityList = null;
// configuration object of fix connection.
protected Connection conn = null;
// Fix connection information
protected ConnectionInfo info;
// mina protobuf session id.
private SessionID sessionId;
// mina protobuf session settings.
private SessionSettings settings;
// process requests operation handlers.
private ProcessRequests processMessages;
private Thread prt;
// mongo database handler, where order status machine is persisted.
public final DatabaseController database = new DatabaseController();
// fix initiator handler.
private Initiator initiator;
// variable for sequence number id generation.
private int nextId = 1;
/**
* Start - FixConnection override.
*
* Connect to mongo database, load customisation fix fields, establish
* initiator connection into the broker side and start order processing
* engine.
*
* @param conn Connection settings.
*/
@Override
public void start(Connection conn) {
// keep connection settings inside.
this.conn = conn;
try {
// Open a database connection.
database.connect(ExecutionServerController.settings);
}
catch (UnknownHostException ex) {
logger.error("Could not connect into database: " + ex.getMessage());
}
try {
// check if a customisation file was set.
if(conn.customFile!=null) {
try {
// load customisation fields for each command.
customisation = CustomisationController.load(conn.customFile);
}
catch (JAXBException ex) {
logger.error("Error parsing file [" + conn.customFile + "]: " + ex.getMessage());
}
catch (FileNotFoundException ex) {
logger.error("Customisation file could not be found: " + ex.getLocalizedMessage());
}
}
// initialise connection to the broker side.
settings = new SessionSettings(conn.configFile);
MessageStoreFactory stf = new FileStoreFactory(settings);
LogFactory lf = new FileLogFactory(settings);
MessageFactory mf = new DefaultMessageFactory();
initiator = new SocketInitiator(this, stf, settings, lf, mf);
initiator.start();
Properties defaultProps = settings.getDefaultProperties();
Iterator it = settings.sectionIterator();
// keep sessionid reference inside.
if(it.hasNext()) {
sessionId = (SessionID) it.next();
}
// Start messages processing thread
processMessages = new ProcessRequests(this, conn);
prt = new Thread(processMessages);
prt.start();
// instance and set connection information object
info = new ConnectionInfo();
info.name = conn.name;
info.fixVersion = sessionId.getBeginString();
info.dictionary = defaultProps.getProperty("UseDataDictionary").equalsIgnoreCase("Y") ? true: false;
info.heartBitInterval = Integer.valueOf(defaultProps.getProperty("HeartBtInt"));
info.hostname = defaultProps.getProperty("SocketConnectHost");
info.port = Integer.valueOf(defaultProps.getProperty("SocketConnectPort"));
info.resetOnLogon = defaultProps.getProperty("ResetOnLogon").equalsIgnoreCase("Y") ? true: false;
info.senderCompId = sessionId.getSenderCompID();
info.targetCompId = sessionId.getTargetCompID();
}
catch (ConfigError ex) {
logger.error("Could not initialise and start the fix connection [" + conn.name + "] :" + ex.getMessage());
//ex.printStackTrace();
}
// try to load security list from database
securityList = database.findAllSecurities();
}
/**
* Stop - FixConnection override.
*
* Stop initiator connection and the order processing engine.
*/
@Override
public void stop() {
/**
* Stop initiator connection.
*/
if(initiator != null) {
initiator.stop();
initiator = null;
}
// signalize thread to stop.
processMessages.running = false;
this.orderNotify();
// Wait a sec to thread finalize.
try {
prt.join(1000);
} catch (InterruptedException ex) {
Logger.getLogger(AbstractFixConnection.class.getName()).log(Level.SEVERE, null, ex);
}
// If the thread is still alive.
if(prt.isAlive()) {
// force interruption.
prt.interrupt();
}
}
/**
* processRequest - FixConnection override
*
* Order processing operation. It must be implemented by each FIX version
* concrete class.
*
* @param order Order about to be processed.
* @throws SessionNotFound
*/
@Override
public void processRequest(ExecutionOrder order) throws SessionNotFound {
logger.error("No implementation to handle this order execution");
}
/**
* onCreated - Application override
*
* On new fix session creation.
*
* @param sid FIX session id.
*/
@Override
public void onCreate(SessionID sid) {
}
/**
* onLogon - Application override
*
* On logon to broker side fix acceptor.
*
* @param sid FIX session id.
*/
@Override
public void onLogon(SessionID sid) {
synchronized(database) {
// try to send old stuck messages.
List<ExecutionOrder> stuckOrders = database.findStuck(conn);
// check if there are any stuck messages.
if(stuckOrders == null) {
return;
}
// process each one.
for(ExecutionOrder curr: stuckOrders) {
try {
this.processRequest(curr);
}
catch (SessionNotFound ex) {
logger.error("Error processing order: " + curr.getId() + ", error: " + ex.getMessage());
}
}
}
}
/**
* onLogoff - Application override
*
* On logoff from broker side fix acceptor.
*
* @param sid FIX session id.
*/
@Override
public void onLogout(SessionID sid) {
}
/**
* toAdmin - Application override.
*
* Intercepts administration messages that will be sent to broker side.
*
* @param msg Message handler
* @param sid Fix session id.
*/
@Override
public void toAdmin(Message msg, SessionID sid) {
try {
// retrieve message type.
MsgType msgType = new MsgType();
msg.getHeader().getField(msgType);
// Check if it is a logon type [A]
if(msgType.getValue().equals("A")) {
// Insert custom fields related to logon message, if they exist.
insertCustomFields(msg, "A", null);
}
}
catch (FieldNotFound ex) {
logger.error(ex.getMessage());
}
}
/**
* fromAdmin - Application override
*
* Receive fix administration messages from broker side.
*
* @param msg Fix message handler
* @param sid Fix session id
*
* @throws FieldNotFound
* @throws IncorrectDataFormat
* @throws IncorrectTagValue
* @throws RejectLogon
*/
@Override
public void fromAdmin(Message msg, SessionID sid) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon {
// Retrieve the message type
MsgType msgType = new MsgType();
msg.getHeader().getField(msgType);
// Retrieve transaction type
ExecTransType execTransType = new ExecTransType();
if (msg.isSetField(execTransType)) {
msg.getField(execTransType);
}
/**
* Check if message type is and reject.
*
* 3 - Reject
*/
if (msgType.getValue().equals("3")) {
// Retrieve reason and send to client.
Text rejectReason = new Text();
if (msg.isSetField(rejectReason)) {
msg.getField(rejectReason);
RefTagID refTagId = new RefTagID();
msg.getField(refTagId);
// Retrieve related tag
if(msg.isSetField(refTagId)) {
logger.error("An order has been reject by server: " + rejectReason + " Tag ID [" + refTagId.getValue() + "].");
}
else {
logger.error("An order has been reject by server: " + rejectReason);
}
} else {
logger.error("[ExecutionServer] A reject was sent by market without a clear reason.");
}
}
}
/**
* toAdmin - Application override.
*
* Intercepts application messages that will be sent to broker side.
*
* @param msg Message handler
* @param sid Fix session id.
*/
@Override
public void toApp(Message msg, SessionID sid) throws DoNotSend {
}
/**
* fromAdmin - Application override
*
* Receive fix application messages from broker side.
*
* @param msg Fix message handler
* @param sid Fix session id
*
* @throws FieldNotFound
* @throws IncorrectDataFormat
* @throws IncorrectTagValue
* @throws RejectLogon
*/
@Override
public void fromApp(Message msg, SessionID sid) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType {
}
/**
* send
*
* Send a message to class referenced session id.
*
* @param msg Message about to be sent.
*
* @throws SessionNotFound
*/
protected void send(Message msg) throws SessionNotFound {
Session.sendToTarget(msg, sessionId);
}
/**
* orderNotify - FixConnection override.
*
* Notify that a new order has been received and is ready to be processed,
* or that this Fix connection is about to be stopped.
*/
@Override
public void orderNotify() {
synchronized (database) {
database.notify();
}
}
/**
* start - FixConnection override.
*
* Start object with stored settings references.
*/
@Override
public void start() {
if(this.conn != null && initiator==null) {
this.start(this.conn);
}
}
/**
* loadSecurities - FixConnection override.
*
* Send a security list message to load the symbols of BMF market.
*
* @throws SessionNotFound
*/
@Override
public void loadSecurities() throws SessionNotFound {
logger.error("No implementation to handle this.");
}
/**
* getHostPort - FixConnection
*
* Retrieve server host name and protobuf connection port.
*
* @return hostname:port
*/
@Override
public String getHostPort() {
try {
return settings.getString("SocketConnectHost") + ":" + settings.getString("SocketConnectPort");
}
catch (Exception ex) {
Logger.getLogger(AbstractFixConnection.class.getName()).log(Level.SEVERE, null, ex);
}
return "Error loading connection settints.";
}
/**
* Retrieve connection information.
*
* @return Object ConnectInfo
*/
@Override
public ConnectionInfo getInfo() {
return info;
}
/**
* Order requests processing engine.
*/
private class ProcessRequests implements Runnable {
// running flag.
private boolean running = true;
// Connection points handlers.
Connection connCfg;
FixConnection conn;
/**
* Constructor.
*
* Keep connection references.
* @param conn
* @param connCfg
*/
public ProcessRequests(FixConnection conn, Connection connCfg) {
this.conn = conn;
this.connCfg = connCfg;
}
/**
* Thread entry-point
*/
@Override
public void run() {
// check database connection, leaves, if it not established.
if(!database.isConnect()) {
return;
}
try {
// main loop
while(true) {
synchronized(database) {
// waits until a new order comes.
while(database.noNewOrders(connCfg)) {
database.wait();
// check if notify is a termination command.
if(!running) {
logger.info("Shutdown command invoked. Exiting...");
return;
}
}
// If Fix connection is no longer logged, ignore order.
if(!isLoggedOn()) {
continue;
}
// Pull one order from database
ExecutionOrder order = database.getOne(connCfg);
// Process order.
conn.processRequest(order);
// mark as processed
database.mark(order);
}
}
}
catch(Exception e){
logger.error("The requests processing engine has been forcely interrupted: " + e.getMessage());
}
}
}
/**
* insertCustomFields
*
* Insert customization fields within a Fix message.
*
* @param msg Fix message reference
* @param cmdType Fix message type
*/
protected void insertCustomFields(Message msg, String cmdType, ExecutionOrder order) {
// check if there is any customisation for this communication
if (customisation == null) {
return;
}
for (Command cmd : customisation.commands) {
if(!cmd.type.equals(cmdType)) {
continue;
}
if(cmd.fields != null) {
for(executionserver.domain.Field field : cmd.fields) {
if(order !=null && field.value.startsWith("[") && field.value.endsWith("]")) {
String value;
String property = field.value.replaceAll("\\[", "");
property = property.replaceAll("\\]", "");
try {
value = BeanUtils.getProperty(order, property);
}
catch (Exception ex) {
logger.error("Can't retrieve property [" + property + "] from order: " + ex.getMessage());
continue;
}
if(value == null) {
logger.error("Can't retrieve property [" + property + "] from order.");
continue;
}
msg.setField(new StringField(field.number, value));
}
else {
msg.setField(new StringField(field.number, field.value));
}
}
}
if(cmd.groups != null) {
for(executionserver.domain.Group group: cmd.groups) {
Group newGroup = new Group(group.number, group.fields.get(0).number);
if(group.fields != null) {
for(executionserver.domain.Field field : group.fields) {
if(order !=null && field.value.startsWith("[") && field.value.endsWith("]")) {
String value;
String property = field.value.replaceAll("\\[", "");
property = property.replaceAll("\\]", "");
try {
value = BeanUtils.getProperty(order, property);
}
catch (Exception ex) {
logger.error("Can't retrieve property [" + property + "] from order: " + ex.getMessage());
continue;
}
if(value == null) {
logger.error("Can't retrieve property [" + property + "] from order.");
continue;
}
newGroup.setField(new StringField(field.number, value));
}
else {
newGroup.setField(new StringField(field.number, field.value));
}
}
msg.addGroup(newGroup);
}
}
}
}
}
/**
* isLoggedOn - FixConnection override
*
* @return if a Fix session is logged on.
*/
@Override
public boolean isLoggedOn() {
return initiator !=null && initiator.isLoggedOn();
}
/**
* Generate a serial client order id.
*
* @param clientId
* @return brand new client order id.
*/
protected String generateId(String clientId) {
return clientId + ":" + nextId++;
}
protected char getTimeInForce(int value) {
switch(value) {
case 0:
return TimeInForce.DAY;
case 1:
return TimeInForce.GOOD_TILL_CANCEL;
case 2:
return TimeInForce.AT_THE_OPENING;
case 3:
return TimeInForce.IMMEDIATE_OR_CANCEL;
case 4:
return TimeInForce.FILL_OR_KILL;
case 5:
return TimeInForce.GOOD_TILL_CROSSING;
case 6:
return TimeInForce.GOOD_TILL_DATE;
case 7:
return TimeInForce.AT_THE_CLOSE;
default:
return TimeInForce.DAY;
}
}
}