Fitch & Mather Stocks: Business Logic Layer

Scott Stanfield
Vertigo Software, Inc.

July 1999

Summary: Describes the design and implementation of the FMStocks business logic layer (BLL). (21 printed pages)

Contents

Overview
Class Breakdown
Account.cls
Broker.cls
Ticker.cls
About the Author
For More Information

Overview

The FMStocks business logic layer (or BLL for short) encapsulates all knowledge about Fitch & Mather's process of buying and selling stocks. It doesn't have any knowledge of the database—all database activity is hidden in the data access layer (DAL). See "Fitch & Mather Stocks: Data Access Layer." The BLL enlists the DAL to do the dirty work. Meanwhile, Active Server Pages (ASP) applications or stand-alone Microsoft® Windows® programs in turn can use the BLL directly.

In particular, the BLL handles the intricacies behind buying and selling stocks. For example, what should happen when an account makes a request to buy stock? Should the trade execute right away? What if there are not enough shares? These decisions, or business logic, are found in one place. If there are multiple clients (FMStocks has only one—the web site), then changes to the process can be made in a central location.

Class Breakdown

The BLL has four classes, as shown in Figure 1.

Figure 1. FMStocks_Bus classes

The Version class is for debugging purposes. It is described in detail in the FMStocks DAL document referred to above.

The other three classes each contain semantically related tasks: Account.cls deals with accounts, Ticker.cls handles stock details, and Broker.cls manages broker functions.

The three classes contain a total of 12 methods. Each method in the BLL can be categorized in one of three areas:

  1. Pass-through: Half of the 12 BLL public methods simply pass through to their equivalent DAL methods. For example, the business component FMStocks_Bus Ticker.VerifySymbol simply calls the FMStocks_DB Ticker.VerifySymbol.

  2. Transformation: Two methods take the recordset returned from the DAL and reconfigure the results into either simple scalar return values or a new recordset all together.

  3. Logic: Four methods have hard-core business logic. They are the ones that handle the functions that actually write to the database: buy, sell, and new account.

Occasionally a simple pass-through method gets "upgraded" to a logic-heavy method. This can happen if the business requirements change after the database is designed.

Account.cls

The Account class has 5 public methods:

Method Type Description
VerifyLogin() Pass-through Used by the login web page to verify an e-mail password combination.
ListPositionsForSale() Pass-through Returns a list of positions available for sale. Used by SellStock.asp.
ListPositions() Pass-through Returns the positions owned by an account, suitable for display by Portfolio.asp.
GetSummary() Transformation Converts the recordset returned by the DAL into output scalar parameters. Used by AccountSummary.asp.
Add() Logic Creates a new account with a preset hard-coded balance of $10,000.

VerifyLogin

VerifyLogin's structure is common to all pass-through methods. Because this is the first one in this document, I'll spend a little more time describing how it works.

The code is similar to its cousin in FMStocks_DB Ticker.cls. The difference is that this one does not use Microsoft ActiveX® Data Objects (ADO) to do the work—it uses FMStocks_DB.Ticker.

The first line sets up the error handler that diverts the flow of execution to the bottom of the method if any runtime errors or exceptions crop up. This exception handler cleans up objects constructed through the CtxCreateObject helper function.

Note   Please read "Fitch & Mather Stocks: Data Access Layer" for a complete discussion of the Ctx* helper functions. Both FMStocks_DB and FMStocks_Bus share the same set of helper functions in the common file Context.bas.

The next two lines in VerifyLogin create an instance of FMStocks_DB.Ticker using a common MTS context. Then, the real work takes place in the call to the DAL method VerifyUser. For consistency, it should have been named VerifyLogin to match the Business Layer.

Next, the instance of the FMStocks_DB.Account object was forcibly cleaned up by setting it to nothing. If you forget this line, the object will be cleaned up when you leave the VerifyLogin method. However, in keeping with the Microsoft Transaction Server (MTS) resource-programming model, you want to free any acquired resource as soon as possible.

On the way out, signal to MTS that you are ready to be lobotomized via CtxSetComplete, then exit from the function. The Boolean return value was taken care of during the call to VerifyUser.

VerifyLogin is used by default.asp.

Public Function VerifyLogin(ByVal EMail As String, _ 
    ByVal Password As String, _
    ByRef FullName As Variant, _ 
    ByRef AccountID As Variant) As Boolean

    
    On Error GoTo errorHandler
    VerifyLogin = False       ' Default to *not* verified
    
    Dim obj As FMStocks_DB.Account
    Set obj = CtxCreateObject("FMStocks_DB.Account")
    
    VerifyLogin = obj.VerifyUser(EMail, Password, AccountID, FullName)
    Set obj = Nothing
    
    CtxSetComplete
    Exit Function
    
errorHandler:
    Set obj = Nothing
    CtxRaiseError g_modName,"VerifyLogin(" & EMail & "," & Password & _
      ")"
End Function

ListPositionsForSale

ListPositionsForSale returns a recordset of positions owned by a given account. If that account owns more than one "chunk" of stock in a particular company, these are summed up so they appear as a single purchase.

The recordset returns a list of tickers, the total shares owned for each ticker, and its current price. ListPositionsForSale is used by SellStock.asp.

Public Function ListPositionsForSale(ByVal AccountID As Long) As _ 
    Recordset 
    On Error GoTo errorHandler
    
    Dim obj As FMStocks_DB.Position
    Set obj = CtxCreateObject("FMStocks_DB.Position")
    
    Set ListPositionsForSale = obj.ListForSale(AccountID)
    
    CtxSetComplete
    Exit Function

errorHandler:
    Set obj = Nothing
    CtxRaiseError g_modName, "ListPositionsForSale(" & AccountID & ")"
End Function

ListPositions

Another name mismatch: ListPositions calls the DAL method ListSummary. However, it is similar to ListPositionsForSale in that it returns a list of tickers and shares owned for an account. ListPositionsForSale returns the current price for each symbol; ListPositions returns the average price paid for each share owned. The average price calculation takes into consideration the possibility of different commissions and strike prices.

The calculations in the underlying stored procedure, Position_ListSummary, are fairly complex. It is documented fully in FMStocks Database Design.

Public Function ListPositions(ByVal AccountID As Long) As Recordset
    On Error GoTo errorHandler
    
    Dim objDB As FMStocks_DB.Position
    Set objDB = CtxCreateObject("FMStocks_DB.Position")
    
    Set ListPositions = objDB.ListSummary(AccountID)
    Set objDB = Nothing
    
    CtxSetComplete
    Exit Function

errorHandler:
    Set objDB = Nothing
    CtxRaiseError g_modName, "ListPositions(" & AccountID & ")"
End Function

GetSummary

GetSummary uses three output parameters to return high-level summary information about an account:

GetSummary typifies a transformation-style method. It extracts three fields from the one-row recordset returned by FMStocks_DB.Account.Summary, and returns them to the caller through ByRef parameters.

Warning   If you are using server-side Microsoft JScript® 5.0 or earlier in your ASP code, it can't handle these ByRef, or output, parameters. This is the primary reason why we at Vertigo Software use Microsoft Visual Basic®, Scripting Edition (VBScript) programming language on the server.

The code also maps NULL values to zeros, using a helper function called ZeroNull. For accounts that do not own stocks, the underlying query returns a NULL for MarketValue.

Public Function GetSummary(ByVal AccountID As Long, _
    ByRef MarketValue As Variant, _
    ByRef TotalInvestment As Variant, _
    ByRef CashBalance As Variant)

    On Error GoTo errorHandler
        
    Dim objAccount As FMStocks_DB.Account
    Set objAccount = CtxCreateObject("FMStocks_DB.Account")
    
    Dim rs As ADODB.Recordset
    Set rs = objAccount.Summary(AccountID)
    Set objAccount = Nothing
     
    MarketValue = ZeroNull(rs("MarketValue").Value)
    TotalInvestment = ZeroNull(rs("TotalInvestment").Value)
    CashBalance = ZeroNull(rs("CashBalance").Value)
    
    CtxSetComplete
    Exit Function

errorHandler:
    MarketValue = 0
    TotalInvestment = 0
    CashBalance = 0
    
    Set rs = Nothing
    Set objAccount = Nothing
    
    CtxRaiseError g_modName, "GetSummary(" & AccountID & ")"
End Function

Private Function ZeroNull(ByVal x As Variant) As Currency
    If IsNull(x) Then
        ZeroNull = 0
    Else
        ZeroNull = x
    End If
End Function

Add

The Account.Add method creates new accounts using the four parameters and returns the new AccountID. The small bit of logic in this method that distinguishes it from a simple pass-through is the fact that it hard-codes the default opening balance. Fitch & Mather are swell folks: instead of giving away toasters, they offer new accounts a starting balance of $10,000.

Public Function Add(ByVal FirstName As String, _
            ByVal LastName As String, _
            ByVal Password As String, _
            ByVal EMail As String) As Long
            
    On Error GoTo errorHandler
    
    Dim db As Object 'FMStocks_DB.Account
    Set db = CtxCreateObject("FMStocks_DB.Account")
      
    Dim defBalance As Currency
    defBalance = "10000"
    
    Add = db.Add(FirstName, LastName, Password, EMail, defBalance)
    Set db = Nothing
    
    CtxSetComplete
    Exit Function
    
errorHandler:
    Set db = Nothing
    CtxRaiseError g_modName, "Add"
End Function

Broker.cls

The Broker class has 3 public methods:

Method Type Description
BuyStock() Logic Adds the buy order to the transaction table, then proceeds to execute the order using ExecuteTransaction.
SellStock() Logic Adds the sell order to the transaction table, then executes it using ExecuteTransaction.
ExecuteTransaction() Logic Although this method is marked public, it is only used by the two methods above.

BuyStock

When a user enters a trade order to buy a particular stock, the BuyStock.asp logic uses BuyStock() to log and execute the trade. In order to fully understand BuyStock and SellStock, it is important to realize that logging a request to buy or sell some stock is different than the execution of that order.

The original intent of this architecture was that the web pages would log the buy and sell orders in the transaction table using BuyStock and SellStock. A separate "broker" Visual Basic program (Broker.exe) used a timer to periodically execute the next trade in line.

During performance testing, Broker was removed from the picture, and the trade right was executed after it was logged. The line in bold below was the only new line necessary to execute the trade after it was logged.

The AddBuyOrder is a DAL method that calls on the Tx_AddBuyOrder stored procedure to create a new entry in the transaction table. The real work occurs in ExecuteTransaction, which is discussed after SellStock.

' returns 0 on nothing to do, -1 on error, otherwise the new 
' TransactionID
Public Function BuyStock(ByVal AccountID As Long, _
    ByVal Ticker As String, _
    ByVal Shares As Long) As Long

        
    On Error GoTo errorHandler
    
    Dim obj As FMStocks_DB.Tx
    Set obj = CtxCreateObject("FMStocks_DB.Tx")
    
    Dim txid As Long
    ' first log the transaction
    txid = obj.AddBuyOrder(AccountID, Ticker, Shares)      
    Set obj = Nothing
    
    BuyStock = ExecuteTransaction(txid)           ' now execute the trade

    
    CtxSetComplete
    Exit Function
    
errorHandler:
    Set obj = Nothing
    CtxRaiseError g_modName, "BuyStock"
End Function

SellStock

SellStock is the flip side of BuyStock. It logs the sell request to the Transaction table via the DAL Tx.AddSellOrder method. The line in bold uses the ExecuteTransaction method to do the actual work.

' returns 0 on nothing to do, -1 on error, otherwise the TxID
Public Function SellStock(ByVal AccountID As Long, _
    ByVal Ticker As String, _
    ByVal Shares As Long) As Long 
        
    On Error GoTo errorHandler
    
    Dim obj As FMStocks_DB.Tx
    Set obj = CtxCreateObject("FMStocks_DB.Tx")
    
    Dim txid As Long
    txid = obj.AddSellOrder(AccountID, Ticker, Shares)
    Set obj = Nothing
    
    SellStock = ExecuteTransaction(txid)
    
    CtxSetComplete
    Exit Function
    
errorHandler:
    Set obj = Nothing
    CtxRaiseError g_modName, "SellStock"
End Function

ExecuteTransaction

By far the most complicated business logic in all of FMStocks, ExecuteTransaction is responsible for executing a single buy or sell request. The only parameter it takes is the transaction ID (TxID). It returns –1 to indicate an error.

Note   Before continuing, I need to make clear a possible source of confusion. My database schema includes a poorly named table called TRANSACTIONS. Please don't confuse this with the SQL concept of a "transaction" or the MTS concept of a "transaction context." The name ExecuteTransaction refers to a buy or sell request that was logged in the transaction TABLE.

The first half of ExecuteTransaction employs a few objects to gather information like the current price for the given ticker, and determines if TxID refers to a sell order or a buy order.

The second half is a big IF statement that splits up the Buy logic from the Sell logic. Both cases have special handling to handle the potential problem of a) not having enough money and b) trying to sell shares you don't really own.

What if there's not enough money?

It's fairly easy to detect the two conditions above. Temporarily overriding the error handling allows you to inspect the Err object directly. The error number for common scenarios can be coded for. Originally, I used a method in FMStocks_DB.Tx to set the transaction type to "insufficient funds" or "not enough shares." It wasn't until I started developing the business objects, however, that I realized I had created a scenario that required a little more knowledge about MTS and the way nested transactions work.

As it turns out, the trigger on the positions table can cause the Broker_Buy stored procedure to roll back. This is actually by design—I wanted the database to ensure that the account's balance never dips below zero. Enforcing this constraint in a trigger seemed a good place for that.

During the business tier processing of a particular buy request, insufficient funds can cause the entire transaction to roll back. I was catching the rollback in the processing logic and using the SetTxType to flag the purchase request as "Insufficient Funds." However, that change to the database was also rolled back!

Because the call to SetTxType didn't occur in its own transaction context, it was caught up in the big rollback initiated by the trigger. In other words, once one of the components votes to abort a transaction, everything in that transaction's context is rolled back.

Solving this problem was easy: I created a new class tagged as Requires a New Transaction, so it always executes independently of any other MTS context.

Figure 2 shows the error condition states, and their TransactionTypeID, in red. Coming into this function, a TransactionID must be in either state 1 or 2.

Figure 2. Transaction record states

This is the only occurrence of nested transactions. The complete source code is shown here:

' Used by ExecuteTrade. Also called directly from BuyStock above
' returns 0 on nothing to do, -1 on error, otherwise the TxID

Public Function ExecuteTransaction(ByVal txid As Long) As Long
    On Error GoTo errorHandler
    
    Dim rs As Recordset
    Dim objTx As FMStocks_DB.Tx
    Dim objTicker As FMStocks_DB.Ticker
    Dim objBroker As FMStocks_DB.Broker
    
    Dim AccountID As Long
    Dim Ticker As String
    
    Set objTicker = CtxCreateObject("FMStocks_DB.Ticker")
    Set objTx = CtxCreateObject("FMStocks_DB.Tx")
    Set objBroker = CtxCreateObject("FMStocks_DB.Broker")
   
    ' Get the Tx details
    Set rs = objTx.GetByID(txid)
    If (rs.EOF And rs.BOF) Then Exit Function
    AccountID = rs("AccountID")
    Ticker = RTrim(rs("Ticker").Value)
    
    ' Only execute buy or sell orders!
    If rs("TransactionTypeID") <> 1 And rs("TransactionTypeID") <> 2 Then
        ExecuteTransaction = 0
        CtxSetComplete
        Exit Function
    End If
    
    'Get the current price
    Dim price As Currency
    price = objTicker.GetPrice(Ticker)      ' price in pennies
    
    'Execute the trade
    Dim positionid As Long
    Dim o As FMStocks_DB.TxNew
    
    Dim lErr As Long
    If rs("TransactionTypeID") = 1 Then
        ' Execute Buy order
       
        On Error Resume Next
        positionid = objBroker.Buy(txid, AccountID, Ticker, _
                     rs("shares"), price, g_commission)

        lErr = Err.Number   
        ' the next line resets Err.Number to 0, so we need to cache it
        On Error GoTo errorHandler
        
        If positionid = 0 And lErr = g_errInsufficentFunds Then
            Err.Clear
            
            ' It's common that the user doesn't have enough funds.
            ' Set the TransactionTypeID = 5 indicating insufficient funds
            
            ' This guy runs as a separate transaction
            Set o = CtxCreateObject("FMStocks_DB.TxNew")
            o.SetTxType txid, 5
            Set o = Nothing
            ExecuteTransaction = 0   ' Signify that we handled this error

                   
        ElseIf positionid = 0 Then
            ' some other error was thrown that we can't handle
            GoTo errorHandler
        Else
            ExecuteTransaction = txid     ' The trade was handled
        End If
    Else
        ' Execute Sell order
        Dim retval As Boolean
        
        On Error Resume Next
        retval = objBroker.Sell(txid, price, g_commission)
        lErr = Err.Number ' the next line resets Err.Number to 0, so we  
                          ' need to cache it        

        On Error GoTo errorHandler
        
        If retval = False Then
            Err.Clear
            
            ' This runs as a separate transaction
            Set o = CtxCreateObject("FMStocks_DB.TxNew")
            ' User tried to sell shares they didn't own
            o.SetTxType txid, 6   
            Set o = Nothing
            ExecuteTransaction = 0   ' Signify that we handled this error
        Else
            ' We're responsible for removing the appropriate shares from
            ' their positions
            AdjustPositions AccountID, Ticker, rs("shares")

            ExecuteTransaction = txid
        End If
        
    End If

    CtxSetComplete
    Exit Function
    
errorHandler:
    ExecuteTransaction = -1
    Set objTx = Nothing
    Set objTicker = Nothing
    Set objBroker = Nothing
    
    CtxRaiseError g_modName, "ExecuteTransaction"
End Function

AdjustPositions

If a sell request is successful, you have to adjust entries in the positions table. Because the positions table holds multiple entries for a given account and ticker, it is tricky to decrement the quantity owned of a particular ticker.

AdjustPositions is best illustrated by an example. Assume you bought Microsoft stock (MSFT) in two batches, say 5 shares and 10 shares. Both purchases exist in the positions table as shown in Figure 3.

Figure 3. Positions table before selling 7 shares

Now say you want to sell 7 shares. What should become of positions #30016 and #10101? The algorithm below uses FMStocks_DB.Position's ListForAdjustment method to return a connected, updateable recordset that you walk through and modify to result in the records shown in Figure 4.

Figure 4. After selling 7 shares

The most recently purchased set of shares was decremented by the number of shares requested to be sold. So the second purchase of 10 shares was decremented by 7, resulting in 3 shares.

Now if you sell 6 shares, PositionID #30016 can be deleted, and #10101 will be lowered to 2, as shown in Figure 5.

Figure 5. Selling 6 more shares

If you were to sell the remaining 2 shares, position #10101 would be deleted.

The do loop in the code below walks through each record in the list, trying to reconcile the number of shares to be sold with how many currently exist. The code will either delete an entire record, or decrement a record's current share amount. At the end of the loop, the recordset changes are committed via UpdateBatch.

This method shows how thin the wall is between the BLL and the DAL. Although the recordset originates from the DAL, it is modified and updated via the BLL. AdjustPositions is the only method in FMStocks that uses updateable recordsets.

Private Sub AdjustPositions(ByVal AccountID As Long, ByVal Ticker As _
    String, ByVal Shares As Long)

    Dim objPosition As FMStocks_DB.Position
    Dim rs As ADODB.Recordset
    Dim posShares As Long ' number of shares for the current position
    
    Set objPosition = CtxCreateObject("FMStocks_DB.Position")
    
    Set rs = objPosition.ListForAdjustment(AccountID, Ticker)
    Do While (Not (rs.EOF) And Shares > 0)
        posShares = rs("shares")
        
        If posShares <= Shares Then
            ' we can delete this whole record
            rs.Delete
            Shares = Shares - posShares
        Else
            ' decrement current share amount by amount sold
            rs("shares").Value = posShares – Shares    
            Shares = 0
        End If
        
        rs.MoveNext
    Loop
    rs.UpdateBatch
    rs.Close
    Set rs = Nothing
End Sub

Ticker.cls

The Ticker class has 4 public methods:

Method Type Description
VerifySymbol Pass-Through Returns true if the stock symbol exists in the database and can be purchased.
ListByTicker Pass-Through Returns a list of stock symbols that begin with the letters passed as the first argument.
ListyByCompany Pass-Through Returns a list of companies whose names begin with the letters passed as the first argument.
GetFundamentals Transformation/Logic Applies a simple stock-screening algorithm to a given stock.

VerifySymbol

VerifySymbol returns true if the symbol was located in the database. Used by BuyStock.asp.

Public Function VerifySymbol(ByVal Ticker As String) As Boolean
    On Error GoTo errorHandler
    Dim obj As FMStocks_DB.Ticker
    Set obj = CtxCreateObject("FMStocks_DB.Ticker")
    
    VerifySymbol = obj.VerifySymbol(Ticker)
    Set obj = Nothing
    
    CtxSetComplete
    Exit Function
    
errorHandler:
    Set obj = Nothing
    CtxRaiseError g_modName, "VerifySymbol"
End Function

ListByTicker

ListByTicker returns a recordset of company names and ticker symbols for all tickers that start with the characters in the first parameter.

Public Function ListByTicker(ByVal Ticker As String) As ADODB.Recordset
    On Error GoTo errorHandler
    Dim obj As FMStocks_DB.Ticker
    Set obj = CtxCreateObject("FMStocks_DB.Ticker")
    
    Set ListByTicker = obj.ListByTicker(Ticker)
    Set obj = Nothing
    
    CtxSetComplete
    Exit Function
    
errorHandler:
    Set obj = Nothing
    CtxRaiseError g_modName, "ListByTicker"
End Function

ListByCompany

ListByCompany returns a recordset of company names and ticker symbols for all companies whose name matches the first parameter.

' ListByCompany
'
Public Function ListByCompany(ByVal company As String) As ADODB.Recordset
    On Error GoTo errorHandler
    Dim obj As FMStocks_DB.Ticker
    Set obj = CtxCreateObject("FMStocks_DB.Ticker")
    
    Set ListByCompany = obj.ListByCompany(company)
    Set obj = Nothing
    
    CtxSetComplete
    Exit Function
        
errorHandler:
    Set obj = Nothing
    CtxRaiseError g_modName, "ListByCompany"
End Function

GetFundamentals

The GetFundamentals function returns a report on a stock's fundamental data in the form of a recordset. Fundamentals refer to a stocks basic quarterly and yearly performance data. The DAL returns a "wide" recordset of one row; the logic below pivots the data into multiple rows with short and long descriptions of each data point. More importantly, each fundamental is tested against a hard-coded criterion that determines if a given stock meets the stringent requirements for a small-cap growth stock.

Visit The Motley Fool's web site at www.fool.com/, and click on the big button labeled Foolish Eight Stock Screener for details on each criteria.

This method differs from all the others in that it creates its own ADO Recordset object and populates it "by hand." The trick to creating a recordset is to open it with the adLockBatchOptimistic flag, and call UpdateBatch when you've finished building it, otherwise you won't be allowed to save your changes. One more tip: reset the recordset to the first item by calling MoveFirst.

Public Function GetFundamentals(ByVal Ticker As String, ByRef company _ 
    As Variant, ByRef Exchange As Variant) As ADODB.Recordset 


    On Error GoTo errorHandler
    Dim obj As FMStocks_DB.Ticker
    Set obj = CtxCreateObject("FMStocks_DB.Ticker")
 
    Dim rs As ADODB.Recordset
    Set rs = obj.GetFundamentals(Ticker)
    
    ' Construct a new recordset based on the results from the fundamental
    ' queries

    
    Dim rsnew As New ADODB.Recordset
    rsnew.CursorLocation = adUseClient
    
    'Add Some Fields
    rsnew.Fields.Append "ShortDesc", adVarChar, 50
    rsnew.Fields.Append "LongDesc", adVarChar, 100
    rsnew.Fields.Append "Criteria", adVarChar, 50
    rsnew.Fields.Append "Value", adVarChar, 50
    rsnew.Fields.Append "Status", adInteger, 4
       
    rsnew.Open , , adOpenStatic, adLockBatchOptimistic
    
    Do While Not (rs.EOF)
        company = rs("Company")
        Exchange = rs("Exchange")
        
        FormatItem rsnew, ZeroNull(rs("MarketCap")) < 1000, _
          "MarketCap", "$" & ZeroNull(rs("MarketCap")) & "M", _
          " < $1B", "Total market capitalization less than $1 Billion."
          
        FormatItem rsnew, ZeroNull(rs("Sales")) < 100, "Sales", "$" & _
          ZeroNull(rs("Sales")) & "M", "< $100M", "Gross revenue" & _
          " (total sales) less than $100 Million."

        FormatItem rsnew, ZeroNull(rs("Price")) > 7, "Price", "$" & _
          ZeroNull(rs("Price")), "> $7/share", "Stock price" & _
          " greater than $7 per share."

        FormatItem rsnew, ZeroNull(rs("DailyDollarVol")) > 1, "Daily" & _
          " dollar volume", "$" & ZeroNull(rs("DailyDollarVol")) & _
          "M", "> $1M per day", "At least $100M in shares move on" & _
          " an average day."

        FormatItem rsnew, ZeroNull(rs("SalesGrowth")) > 25, "Sales" & _
          " growth", ZeroNull(rs("SalesGrowth")) & "%", "> 25%", _
          "Gross revenue growth of 25% trailing-twelve months (TTM)."

        FormatItem rsnew, ZeroNull(rs("EPSGrowth")) > 25, "Earnings" & _
          " growth", ZeroNull(rs("EPSGrowth")) & "%", "> 25%", _
          "Earnings-per-share growth over TTM."

        FormatItem rsnew, ZeroNull(rs("NetProfitMargin")) > 7, _
          "Profit margin", ZeroNull(rs("NetProfitMargin")) & "%", _
          "> 7%", "Net profit margin at least 7%."

        FormatItem rsnew, ZeroNull(rs("InsiderShares")) > 10, _
          "Insider holdings", ZeroNull(rs("InsiderShares")) & "%", _
          "> 10%", "At least 10% of the shares should be held by" & _
          " insiders."
            
        rs.MoveNext
    Loop

    rsnew.UpdateBatch adAffectAll
    rsnew.MoveFirst
    
    Set obj = Nothing
    Set rs = Nothing
    Set GetFundamentals = rsnew
    CtxSetComplete
    Exit Function
        
errorHandler:
     obj = Nothing
    CtxRaiseError g_modName, "GetFundamentals(" & Ticker & ")"
End Function

Sub FormatItem(ByRef rs As ADODB.Recordset, Status As Integer, _
               ShortDesc As String, Value As String, _
               Criteria As String, LongDesc As String)
    
    rs.AddNew Array("ShortDesc", "LongDesc", "Criteria", "Value", _
                    "Status"), _
              Array(ShortDesc, LongDesc, Criteria, Value, Status)

End Sub

About the Author

Scott Stanfield is the president of Vertigo Software, Inc. Vertigo Software is a San Francisco Bay Area-based consulting firm that specializes in the design and development of Windows Distributed interNet Architecture (DNA) applications and components. He can be reached at scott@vertigosoftware.com or on the Web at http://www.vertigosoftware.com/.

For More Information